corentin_wakdo/docs/merise/dictionary.md
Corentin JOGUET ad5203d3fc
All checks were successful
CI / secret-scan (push) Successful in 8s
CI / php-lint (push) Successful in 20s
CI / static-tests (push) Successful in 32s
CI / auto-merge (push) Has been skipped
feat(admin): throttle du PIN d action sensible par acteur (RG-T22) (#18)
2026-06-16 00:06:33 +02:00

57 KiB
Raw Permalink Blame History

Dictionnaire de Donnees — Wakdo

Phase Merise : P1 - Conception, etape 1 (dictionnaire de donnees d'abord, mantra #33) Version : v0.3 — prod-like, 22 entites (19 prod-like + couche security-by-design, incl. les entites login_throttle et pin_throttle) Date : 2026-06-04 (ajouts security-by-design 2026-06-11) Branche : feat/p1-conception Statut : prod-like — toutes les decisions D1-D8 + stock appliquees (voir docs/notes/revue-alignement-p1.md §7) ; couche security-by-design en cours (voir note 13) Auteur : BYAN (couche methodologie)


1. Objectif

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 (mapping 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 du menu, flux de commande, RBAC, modes de service)
  • Maquette : docs/design/maquette-borne.pdf (UX borne, ecrans visibles)

Tous les ecarts entre la source ecole et le modele final sont documentes dans la section "Notes de modelisation" en bas de ce document.

Pour le diagramme entite-relation et les justifications de cardinalite, voir mcd.md. Ce dictionnaire ne duplique pas cette vue afin d'eviter des sources de verite divergentes.


2. Conventions generales

Nommage

  • Tables : snake_case, singulier (ex. category, product, customer_order). Le singulier reflete la perspective "1 ligne = 1 instance de l'entite" (convention relationnelle standard). Le code applicatif (PHP, JS) utilise ces noms tels quels via le mapping ORM.
  • Colonnes : snake_case. Suffixes typiques : _id (FK), _at (timestamp), _cents (montant monetaire en centimes entiers), _path (chemin de fichier), _rate (taux ou fraction stocke en entier pour-mille).
  • Cles primaires : colonne id (INT UNSIGNED AUTO_INCREMENT). Pas de PK composite sauf sur les tables de jointure pures.
  • Cles etrangeres : <referenced_table>_id (ex. category_id dans product).
  • Valeurs ENUM : anglais, snake_case (ex. pending_payment, dine_in, kiosk).
  • Chaines cote code (ENUM, codes de permission, codes de role) : anglais uniquement, coherentes entre la BDD, PHP et l'API JSON.

Types par defaut

Categorie Type MariaDB Justification
Identifiants INT UNSIGNED AUTO_INCREMENT 4 milliards d'ids — suffisant pour ce projet
Libelles courts VARCHAR(120) Couvre la plupart des noms de produits (max observe : 41 caracteres dans la source ecole)
Descriptions TEXT Longueur variable, sans limite stricte
Montants monetaires INT UNSIGNED (cents) Evite les bugs d'arrondi FLOAT (voir note 1)
Booleens TINYINT(1) Convention MariaDB pour BOOLEAN (alias)
Horodatages DATETIME Lisible par l'humain, fuseau horaire gere au niveau applicatif
Enumerations ENUM('a','b','c') Contrainte au niveau SGBD, lisible (voir note 2)
Chemins de fichiers VARCHAR(255) Limite standard de longueur de chemin POSIX

Charset et collation

  • Charset : utf8mb4 (RFC 3629 — vrai UTF-8 sur 4 octets, supporte emoji et caracteres asiatiques). MariaDB gere utf8mb4 nativement.
  • Collation : utf8mb4_unicode_ci (insensible a la casse, comparaison conforme Unicode).

Champs d'audit (presents sur toutes les tables metier sauf les tables de jointure pures)

Colonne Type Default Role
created_at DATETIME CURRENT_TIMESTAMP Timestamp de creation, ecrit une fois a l'insertion
updated_at DATETIME CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP Timestamp de derniere modification, mis a jour automatiquement

Soft delete

Pas de soft delete generalise. Les entites qui peuvent etre temporairement desactivees portent une colonne booleenne is_active ou is_available. Le DELETE dur reste possible mais est reserve aux operations admin avec sauvegarde prealable.


3. Entites

3.1 category

Regroupement metier de produits et de menus pour l'affichage sur la borne.

Attribut Type NULL Default Contrainte Source ecole Notes
id INT UNSIGNED NO AUTO_INCREMENT PK id (1-9) identique a la source
name VARCHAR(60) NO UNIQUE title renomme depuis title
slug VARCHAR(60) NO UNIQUE derive de title (kebab-case minuscule) utilise pour l'URL /api/categories/burgers
image_path VARCHAR(255) YES NULL image chemin relatif, voir note 8
display_order SMALLINT UNSIGNED NO 0 (ajoute) ordre d'affichage sur la borne, ajustable depuis l'admin
is_active TINYINT(1) NO 1 (ajoute) desactiver sans supprimer
created_at DATETIME NO CURRENT_TIMESTAMP audit
updated_at DATETIME NO CURRENT_TIMESTAMP ON UPDATE audit

Exemples : menus, drinks, burgers, fries, snacks, wraps, salads, desserts, sauces. Volume : 9 lignes a l'init (seed depuis categories.json).


3.2 product

Un article vendable unique, disponible a la carte ou comme composant dans un slot de menu.

Attribut Type NULL Default Contrainte Source ecole Notes
id INT UNSIGNED NO AUTO_INCREMENT PK id identique a la source
category_id INT UNSIGNED NO FK -> category(id), ON DELETE RESTRICT (derive de la cle d'objet JSON)
name VARCHAR(120) NO INDEX nom renomme depuis nom
description TEXT YES NULL (ajoute) renseigne plus tard via l'admin
price_cents INT UNSIGNED NO CHECK > 0 prix (FLOAT) conversion FLOAT -> INT centimes au seed (voir note 1)
vat_rate SMALLINT UNSIGNED NO 100 CHECK IN (55, 100) (ajoute) taux de TVA en pour-mille : 100 = 10%, 55 = 5,5%. Defaut 10%. Voir note 9
image_path VARCHAR(255) YES NULL image chemin relatif, voir note 8
is_available TINYINT(1) NO 1 (ajoute) bascule de disponibilite manuelle depuis l'admin
display_order SMALLINT UNSIGNED NO 0 (ajoute) ordre d'affichage au sein de 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 13 menus deplaces vers menu).


3.3 menu

Combo a prix fixe construit autour d'un burger specifique, avec des slots selectionnables par le client (boisson, accompagnement, sauce). Deux paliers de prix : Normal et Maxi.

Attribut Type NULL Default Contrainte Source ecole Notes
id INT UNSIGNED NO AUTO_INCREMENT PK id (1-13 dans la categorie menus)
category_id INT UNSIGNED NO FK -> category(id), ON DELETE RESTRICT implicite (categorie menus)
burger_product_id INT UNSIGNED NO FK -> product(id), ON DELETE RESTRICT (ajoute) le burger fixe qui ancre ce menu ; pilote la personnalisation des ingredients
name VARCHAR(120) NO INDEX nom ex. "Menu Le 280"
description TEXT YES NULL (ajoute)
price_normal_cents INT UNSIGNED NO CHECK > 0 prix prix format Normal. Remplace le prix_ttc_cents unique.
price_maxi_cents INT UNSIGNED NO CHECK > 0 (ajoute) prix format Maxi (~+150 centimes vs normal ; voir note 7)
image_path VARCHAR(255) YES NULL image reutilise generalement l'image du burger
is_available TINYINT(1) NO 1 (ajoute)
display_order SMALLINT UNSIGNED NO 0 (ajoute)
created_at DATETIME NO CURRENT_TIMESTAMP audit
updated_at DATETIME NO CURRENT_TIMESTAMP ON UPDATE audit

Volume : 13 lignes a l'init. Remplace l'ancien modele menu_produit a composition fixe.


3.4 menu_slot

Un slot selectionnable au sein d'un menu (ex. "slot boisson", "slot accompagnement", "slot sauce"). Chaque slot contraint les produits parmi lesquels le client peut choisir, exprimes via la table de jointure menu_slot_option.

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
menu_id INT UNSIGNED NO FK -> menu(id), ON DELETE CASCADE un slot appartient a exactement un menu
name VARCHAR(80) NO ex. "Drink", "Side", "Sauce"
slot_type ENUM('drink','side','sauce','dessert','extra') NO role semantique de ce slot
is_required TINYINT(1) NO 1 indique si le client doit remplir ce slot
display_order SMALLINT UNSIGNED NO 0 ordre d'affichage dans le constructeur de menu

Pas de champs d'audit : un slot fait partie de la definition du menu ; cree et mis a jour avec le menu. Index composite : (menu_id, display_order).


3.5 menu_slot_option

Produits eligibles pour un slot de menu donne. Table de jointure pure.

Attribut Type NULL Default Contrainte Notes
menu_slot_id INT UNSIGNED NO FK -> menu_slot(id), ON DELETE CASCADE
product_id INT UNSIGNED NO FK -> product(id), ON DELETE RESTRICT RESTRICT : retirer un produit ne doit pas casser silencieusement les menus

Cle primaire : composite (menu_slot_id, product_id).

Volume : ~3-5 options par slot, ~3 slots par menu, 13 menus = ~120-200 lignes a l'init.


3.6 ingredient

Ingredient elementaire utilise dans la composition des produits. Porte les donnees de stock.

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
name VARCHAR(120) NO UNIQUE ex. "Sesame Bun", "Cheddar Slice", "Ketchup Portion"
unit VARCHAR(40) NO libelle de l'unite de conditionnement : piece / portion / sachet 1kg / pot / bouteille (libelle libre, pas un ENUM — les unites varient par ingredient)
stock_quantity INT (signed) NO 0 stock courant en unites. INT signe sans CHECK >= 0 : il PEUT devenir negatif quand les ventes depassent le stock compte (ampleur de la survente, remontee aux managers). Le systeme ne bloque pas une commande sur le stock.
stock_capacity INT NO CHECK > 0 niveau "plein" de reference en unites = les 100% servant a calculer le pourcentage de stock. Le CHECK > 0 protege aussi la division du pourcentage contre la division par zero
pack_size SMALLINT UNSIGNED NO 1 CHECK > 0 unites par pack de reapprovisionnement (ex. 100 pour un sac de 100 portions)
pack_label VARCHAR(80) YES NULL libelle humain du pack (ex. "Sac 100 portions")
low_stock_pct SMALLINT UNSIGNED NO 10 CHECK BETWEEN 0 AND 100 bande dalerte, pourcentage de capacite : stock_quantity <= stock_capacity * low_stock_pct/100 declenche l'indicateur de stock bas
critical_stock_pct SMALLINT UNSIGNED NO 5 CHECK BETWEEN 0 AND 100 seuil de rupture automatique, pourcentage de capacite : stock_quantity <= stock_capacity * critical_stock_pct/100 rend le produit calcule en rupture
is_active TINYINT(1) NO 1 desactiver les ingredients obsoletes sans supprimer
created_at DATETIME NO CURRENT_TIMESTAMP audit
updated_at DATETIME NO CURRENT_TIMESTAMP ON UPDATE audit

CHECK au niveau table : critical_stock_pct < low_stock_pct (le seuil critique se situe sous la bande dalerte).

Regle de decrement de stock : a la transition paid, chaque ingredient est decremente de product_ingredient.quantity_normal ou quantity_maxi (selectionne par order_item.format) multiplie par order_item.quantity, puis ajuste par les lignes order_item_modifier. Voir note 7. Regle de reapprovisionnement : stock_quantity += N * pack_size (reapprovisionne en packs complets). Regle d'annulation : le stock est recredite quand une commande paid est annulee. Modele de stock (base sur le pourcentage, trois bandes) : le seuil d'alerte absolu est remplace par un modele en pourcentage ancre sur stock_capacity (la reference 100%). Le pourcentage de stock est calcule, non stocke : stock_pct = ROUND(stock_quantity / stock_capacity * 100). Le CHECK > 0 sur stock_capacity protege cette division contre la division par zero. Trois bandes :

  • Normal — au-dessus de la bande dalerte : rien n'est signale.
  • Lowstock_quantity <= stock_capacity * low_stock_pct/100 : commandable + alerte manager. Le manager retire le produit via product.is_available=0, ou reapprovisionne pour lever l'alerte.
  • Criticalstock_quantity <= stock_capacity * critical_stock_pct/100 : le produit passe automatiquement en rupture (disponibilite calculee, voir regle RG-T21 dans mlt.md) ; aucune colonne stockee supplementaire.

3.7 product_ingredient

Composition par defaut d'un produit (burger, wrap, etc.) en termes d'ingredients. Porte les metadonnees de personnalisation pour le configurateur d'ingredients.

Attribut Type NULL Default Contrainte Notes
product_id INT UNSIGNED NO FK -> product(id), ON DELETE CASCADE
ingredient_id INT UNSIGNED NO FK -> ingredient(id), ON DELETE RESTRICT RESTRICT : impossible de retirer un ingredient encore reference dans une recette de produit
quantity_normal SMALLINT UNSIGNED NO 1 CHECK > 0 unites consommees en format Normal (ex. 2 pour double cheese)
quantity_maxi SMALLINT UNSIGNED NO 1 CHECK > 0 unites consommees en format Maxi. Egale quantity_normal pour les ingredients invariants au format (burger, sauce) ; superieure pour les ingredients d'accompagnement et de boisson (le Maxi agrandit uniquement l'accompagnement + la boisson). Voir note 7.
is_removable TINYINT(1) NO 1 le client peut retirer cet ingredient sans frais
is_addable TINYINT(1) NO 0 le client peut ajouter une unite supplementaire de cet ingredient
extra_price_cents INT UNSIGNED NO 0 CHECK >= 0 supplement en centimes quand is_addable=1 et que le client l'ajoute (0 = extra gratuit)

Cle primaire : composite (product_id, ingredient_id).

Volume : ~5-10 ingredients par produit, ~53 produits = ~300-500 lignes au seed.


3.8 allergen

Catalogue des 14 allergenes reglementes (Reglement INCO (UE) 1169/2011).

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
code VARCHAR(30) NO UNIQUE code lisible par machine, ex. gluten, milk, nuts
name VARCHAR(80) NO nom d'affichage, ex. "Gluten", "Lait", "Fruits a coque"
description TEXT YES NULL guidance optionnelle pour le personnel

Volume : 14 lignes au seed (fixe par le reglement UE 1169/2011, liste confirmee au moment du seed). Les allergenes d'un produit sont calcules en joignant product_ingredient -> ingredient_allergen -> allergen ; pas de ressaisie manuelle par produit.


3.9 ingredient_allergen

Indique quels allergenes contient chaque ingredient. Table de jointure pure.

Attribut Type NULL Default Contrainte Notes
ingredient_id INT UNSIGNED NO FK -> ingredient(id), ON DELETE CASCADE
allergen_id INT UNSIGNED NO FK -> allergen(id), ON DELETE RESTRICT

Cle primaire : composite (ingredient_id, allergen_id).


3.10 customer_order

Transaction client : 1 commande = 1 panier valide a un instant donne. (Rationale du nom de table : voir note de modelisation 3.)

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
order_number VARCHAR(20) NO UNIQUE format lisible par l'humain : K/C/D-YYYY-MM-DD-NNN. Prefixe par canal : K=kiosk, C=counter, D=drive. Voir note 4.
idempotency_key VARCHAR(36) YES NULL UNIQUE UUID genere par le client pour dedupliquer un POST /api/orders reessaye (anti-double-charge). UNIQUE rejette les doublons ; plusieurs NULL autorises. Security-by-design, voir note 13
source ENUM('kiosk','counter','drive') NO INDEX canal de saisie (qui a saisi la commande). Valeurs en anglais, voir note 5.
acting_user_id INT UNSIGNED YES NULL FK -> user(id), ON DELETE SET NULL personnel back-office (counter/drive) ayant cree la commande, capture sous PIN. NULL pour kiosk (anonyme). Imputabilite ciblee sans imposer un login par personne sur la borne. Voir note 13
service_mode ENUM('dine_in','takeaway','drive') NO mode de consommation, conserve pour les stats/KPI uniquement. Aucun role fiscal (voir note 9). La source drive implique le service_mode drive (contrainte croisee appliquee au niveau applicatif).
status ENUM('pending_payment','paid','delivered','cancelled') NO 'pending_payment' INDEX machine a 4 etats : pending_payment -> paid -> delivered (+ cancelled). Voir note 6.
total_ht_cents INT UNSIGNED NO CHECK >= 0 total hors TVA, snapshot a la validation de la commande
total_vat_cents INT UNSIGNED NO CHECK >= 0 montant de TVA, snapshot
total_ttc_cents INT UNSIGNED NO CHECK > 0 total TTC ; doit egaler total_ht_cents + total_vat_cents (verifie a la couche MLT)
paid_at DATETIME YES NULL timestamp de la transition vers paid (NULL avant paiement)
delivered_at DATETIME YES NULL timestamp de la transition vers delivered (NULL avant la remise)
cancelled_at DATETIME YES NULL timestamp d'annulation (NULL si non annulee)
created_at DATETIME NO CURRENT_TIMESTAMP INDEX utilise pour les agregations de stats en direct ; sert aussi de base a service_day
updated_at DATETIME NO CURRENT_TIMESTAMP ON UPDATE audit

Retire de v0.1 : tva_taux_pourmille (deplace au niveau ligne — order_item.vat_rate_snapshot), paye_a (renomme paid_at). Etats machine preparing et ready retires (voir note 6).

Calcul de service_day (regroupement KPI) :

CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END

Calcule au moment de la requete, non stocke comme colonne (la formule de colonne generee avec INTERVAL 4 HOUR 30 MINUTE dans le MLD v0.1 etait incorrecte et est retiree). Coupure : 10:00.

Volume : ~100-300 commandes/jour au pic, ~10k lignes sur une demo de 6 mois.


3.11 order_item

Ligne d'une commande : un seul produit ou un menu, avec prix, libelle et taux de TVA snapshotes au moment de la transaction.

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
order_id INT UNSIGNED NO FK -> customer_order(id), ON DELETE CASCADE
item_type ENUM('product','menu') NO discriminateur
product_id INT UNSIGNED YES NULL FK -> product(id), ON DELETE RESTRICT non nul si item_type = 'product'
menu_id INT UNSIGNED YES NULL FK -> menu(id), ON DELETE RESTRICT non nul si item_type = 'menu'
format ENUM('normal','maxi') NO 'normal' s'applique aux items menu (Normal / Maxi). Pour les produits autonomes, la valeur est normal (pas d'agrandissement individuel dans ce modele). Voir note 7.
label_snapshot VARCHAR(120) NO libelle au moment de la commande (preserve si le produit est renomme)
unit_price_cents_snapshot INT UNSIGNED NO CHECK > 0 prix unitaire TTC au moment de la commande
vat_rate_snapshot SMALLINT UNSIGNED NO CHECK IN (55, 100) taux de TVA en pour-mille au moment de la commande (snapshote depuis product.vat_rate)
quantity SMALLINT UNSIGNED NO 1 CHECK > 0 quantite commandee (ex. 3 Cocas = 1 ligne avec quantity=3)
created_at DATETIME NO CURRENT_TIMESTAMP audit

Contrainte CHECK (applicative ou MariaDB CHECK >= 10.2) : (item_type='product' AND product_id IS NOT NULL AND menu_id IS NULL) OR (item_type='menu' AND menu_id IS NOT NULL AND product_id IS NULL)

Volume : ~3-5 lignes par commande -> 30k-50k lignes sur 6 mois.


3.12 order_item_selection

Les choix reels effectues par le client pour chaque slot d'une ligne de menu. 1 ligne = 1 slot rempli pour 1 order_item de type menu.

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
order_item_id INT UNSIGNED NO FK -> order_item(id), ON DELETE CASCADE doit referencer un order_item avec item_type='menu'
menu_slot_id INT UNSIGNED NO FK -> menu_slot(id), ON DELETE RESTRICT quel slot a ete rempli
product_id INT UNSIGNED NO FK -> product(id), ON DELETE RESTRICT produit choisi par le client pour ce slot
label_snapshot VARCHAR(120) NO libelle du produit au moment de la commande

Volume : ~2-3 selections par ligne de menu. Usage KPI : permet d'analyser quelles combinaisons boisson/accompagnement sont les plus choisies.


3.13 order_item_modifier

Modifications au niveau ingredient appliquees par le client a un produit ou au burger fixe d'un menu : retrait (gratuit) ou ajout (avec supplement optionnel).

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
order_item_id INT UNSIGNED NO FK -> order_item(id), ON DELETE CASCADE la ligne de commande modifiee (produit ou menu)
ingredient_id INT UNSIGNED NO FK -> ingredient(id), ON DELETE RESTRICT l'ingredient modifie
action ENUM('remove','add') NO remove = retrait gratuit ; add = unite supplementaire (peut avoir un supplement)
extra_price_cents INT UNSIGNED NO 0 CHECK >= 0 snapshot de product_ingredient.extra_price_cents au moment de la commande (0 pour les retraits)

Regle de rattachement du modificateur (voir note de modelisation 10) :

  • Pour un produit autonome (item_type='product') : le modificateur cible le produit directement via order_item_id.
  • Pour un menu (item_type='menu') : le modificateur cible le burger fixe de la ligne de menu via le meme order_item_id. Le burger est identifie par menu.burger_product_id, permettant a l'affichage cuisine de resoudre sans ambiguite quels ingredients sont modifies. Aucune FK supplementaire n'est necessaire : etant donne order_item_id, le burger est order_item.menu_id -> menu.burger_product_id.

Impact stock : chaque modificateur affecte le stock d'ingredient a la transition paid (remove -> pas de decrement pour cet ingredient ; add -> decrement supplementaire).


3.14 user

Utilisateur back-office (admin, manager, personnel cuisine, counter, drive). Les clients de la borne ne sont pas authentifies et n'ont pas de ligne ici.

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
email VARCHAR(254) NO UNIQUE longueur max selon RFC 5321
password_hash VARCHAR(255) NO hash argon2id (voir PASSWORD_ALGO dans .env) ; longueur typique 96 caracteres, marge jusqu'a 255
first_name VARCHAR(60) NO
last_name VARCHAR(60) NO
role_id INT UNSIGNED NO FK -> role(id), ON DELETE RESTRICT un utilisateur ne peut exister sans role
is_active TINYINT(1) NO 1 desactivation sans suppression
last_login_at DATETIME YES NULL utile pour l'audit et la detection de comptes dormants
pin_hash VARCHAR(255) YES NULL hash argon2id du PIN par membre du personnel qui autorise les actions sensibles (prix/RBAC/utilisateur/annulation/inventaire). NULL = aucun PIN defini. Security-by-design, voir note 13
failed_login_attempts SMALLINT UNSIGNED NO 0 logins echoues consecutifs ; pilote le throttling degressif (note 13)
last_failed_login_at DATETIME YES NULL timestamp du dernier login echoue
lockout_until DATETIME YES NULL fin de la fenetre de throttling courante (backoff degressif, pas un verrouillage dur indefini)
password_reset_token_hash VARCHAR(255) YES NULL hash du token de reset (pas le token brut) ; NULL quand aucun reset en attente
password_reset_expires_at DATETIME YES NULL expiration du token de reset
anonymized_at DATETIME YES NULL marqueur tombstone RGPD : quand renseigne, les colonnes PII sont mises a NULL/remplacees (note 13). La ligne est conservee pour l'integrite referentielle
created_at DATETIME NO CURRENT_TIMESTAMP audit
updated_at DATETIME NO CURRENT_TIMESTAMP ON UPDATE audit

Volume : 5-20 lignes (equipe du restaurant + 1-2 admins).

Longueur d'email RFC 5321 : local-part <= 64, domaine <= 255, total <= 254 (incluant @). VARCHAR(254) est la valeur conforme a la spec.

Colonnes PII : email, first_name, last_name. Soumises a l'anonymisation RGPD (voir note 13). password_hash et pin_hash sont des credentials, tenus hors des logs et des reponses d'API.


3.15 role

Roles back-office (RBAC). Creables / modifiables / desactivables depuis l'UI admin. Le seed fournit 5 roles ; des roles personnalises (ex. "chef-patissier") peuvent etre ajoutes sans deploiement.

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
code VARCHAR(40) NO UNIQUE code machine, ex. admin, manager, kitchen, counter, drive
label VARCHAR(80) NO nom d'affichage, ex. Administrator, Kitchen Staff
description TEXT YES NULL
default_route VARCHAR(120) YES NULL ecran d'atterrissage pour ce role (ex. /admin/orders, /kitchen/display). Rend le routage dynamique — pas de noms de role en dur dans le routage front-end.
order_source ENUM('kiosk','counter','drive') YES NULL source auto-taggee quand ce role cree une commande (NULL pour admin/manager qui peuvent creer au nom de n'importe quel canal)
is_active TINYINT(1) NO 1 la desactivation preserve l'historique des utilisateurs ayant detenu ce role
created_at DATETIME NO CURRENT_TIMESTAMP audit
updated_at DATETIME NO CURRENT_TIMESTAMP ON UPDATE audit

Roles du seed :

Code default_route order_source
admin /admin/dashboard NULL
manager /admin/stats NULL
kitchen /kitchen/display NULL
counter /counter/orders counter
drive /drive/orders drive

Regle d'architecture RBAC (P2) : le code applicatif teste les permissions, pas les noms de role. Ajouter un nouveau role avec les bonnes permissions ne requiert aucun changement de code (pilote par permission, non par nom de role — selon le modele RBAC Sandhu/NIST).


3.16 role_visible_source

Definit quelles sources de commande sont visibles sur le tableau de bord de preparation pour un role donne. Table de jointure pure.

Attribut Type NULL Default Contrainte Notes
role_id INT UNSIGNED NO FK -> role(id), ON DELETE CASCADE
source ENUM('kiosk','counter','drive') NO source visible pour ce role sur l'affichage kitchen/counter/drive

Cle primaire : composite (role_id, source).

Donnees du seed :

Role Sources visibles
kitchen kiosk, counter, drive (toutes)
counter kiosk, counter
drive drive

3.17 permission

Permissions granulaires assignables aux roles. Le catalogue est fixe au seed (pas de creation via UI).

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
code VARCHAR(60) NO UNIQUE format <resource>.<action>
label VARCHAR(120) NO nom d'affichage
description TEXT YES NULL
created_at DATETIME NO CURRENT_TIMESTAMP audit

Catalogue de permissions fixe (23 codes — gele avant le DDL) :

Code Accorde a (defaut seed)
product.create admin, manager
product.read admin, manager, kitchen, counter, drive
product.update admin, manager
product.delete admin
menu.create admin, manager
menu.read admin, manager, kitchen, counter, drive
menu.update admin, manager
menu.delete admin
category.manage admin, manager
ingredient.manage admin, manager
stock.read admin, manager, kitchen, counter, drive
stock.count admin, manager, kitchen, counter, drive
stock.manage admin, manager
order.read admin, manager, kitchen, counter, drive
order.create admin, counter, drive
order.deliver admin, counter, drive
order.cancel admin, counter, drive
user.create admin
user.read admin, manager
user.update admin
user.deactivate admin
role.manage admin
stats.read admin, manager

Volume : 23 lignes au seed.


3.18 role_permission

Mapping N-N entre roles et permissions. Table de jointure pure.

Attribut Type NULL Default Contrainte Notes
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 au seed (admin couvre tout ; les autres couvrent un sous-ensemble).


3.19 stock_movement

Journal d'audit append-only de tous les changements de stock par ingredient. 1 ligne par mouvement (vente, annulation, reapprovisionnement, correction d'inventaire).

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
ingredient_id INT UNSIGNED NO FK -> ingredient(id), ON DELETE RESTRICT ingredient affecte
movement_type ENUM('sale','cancellation','restock','inventory_correction') NO INDEX nature du mouvement
delta INT NO changement signe : negatif pour la consommation (vente), positif pour reapprovisionnement/annulation/correction
order_id INT UNSIGNED YES NULL FK -> customer_order(id), ON DELETE SET NULL commande liee pour les mouvements sale et cancellation ; NULL pour restock/correction
user_id INT UNSIGNED YES NULL FK -> user(id), ON DELETE SET NULL utilisateur ayant declenche le mouvement (NULL pour les decrements de vente automatises)
note VARCHAR(255) YES NULL note humaine optionnelle (ex. raison de la correction, reference de pack)
created_at DATETIME NO CURRENT_TIMESTAMP INDEX timestamp immuable

Immuabilite : aucun UPDATE ni DELETE sur cette table. Les corrections sont de nouvelles lignes avec movement_type='inventory_correction' et un delta signe.

Mouvements automatiques (declenches aux transitions de statut) :

  • transition paid : 1 ligne sale par unite d'ingredient consommee (en tenant compte des modificateurs).
  • cancelled (depuis paid) : 1 ligne cancellation par unite d'ingredient recreditee.

Mouvements manuels :

  • restock : le manager ou l'admin enregistre une livraison (+= N * pack_size).
  • inventory_correction : comptage physique matin/soir ; le systeme enregistre l'ecart (delta = reel - theorique).

Volume : ~5-15 mouvements par commande sur tous les ingredients ; un index sur (ingredient_id, created_at) est recommande pour les requetes d'historique par ingredient.


3.20 audit_log

Journal append-only des actions back-office sensibles, pour l'imputabilite la ou elle importe (menace interne, manipulation d'argent, changements RBAC). Complete stock_movement (specifique au stock) ; couvre les evenements catalogue/prix, utilisateur, role/permission et annulation de commande. Ajout security-by-design (voir note 13).

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
actor_user_id INT UNSIGNED YES NULL FK -> user(id), ON DELETE SET NULL personnel ayant effectue l'action, capture via PIN pour les operations sensibles. NULL si non attribuable a un individu
actor_role_id INT UNSIGNED YES NULL FK -> role(id), ON DELETE SET NULL contexte de role au moment de l'action (denormalise pour que la trace survive a l'anonymisation de l'utilisateur)
action_code VARCHAR(60) NO INDEX code d'operation MCT / de permission, ex. product.update, order.cancel, role.manage, user.deactivate
entity_type VARCHAR(40) YES NULL nom de la table affectee, ex. product, customer_order, role, user
entity_id INT UNSIGNED YES NULL PK de la ligne affectee
summary VARCHAR(255) YES NULL courte description non personnelle, ex. "price_cents 880 -> 920", "added permission stock.manage"
details JSON YES NULL diff before/after optionnel. Pour les actions ciblant un utilisateur, stocke les noms de champs modifies, pas les valeurs PII
created_at DATETIME NO CURRENT_TIMESTAMP INDEX timestamp immuable

Immuabilite : aucun UPDATE ni DELETE au niveau applicatif (meme discipline que stock_movement). Index : (actor_user_id, created_at), (entity_type, entity_id), (action_code, created_at). Retention : fenetre propre (~12 mois, interet legitime / tracabilite fiscale), decouplee du cycle de vie des PII utilisateur (note 13). Une purge planifiee (cron) retire les lignes au-dela de la fenetre.

Operations journalisees (ensemble sensible) : UPDATE_PRODUCT (8.2, incl. prix), DELETE_PRODUCT (8.3), DELETE_MENU (8.6), CANCEL_ORDER (7.1), RESTOCK (9.1), INVENTORY_COUNT (9.2), CREATE_USER / UPDATE_USER / DEACTIVATE_USER (10.1-10.3), MANAGE_RBAC (10.4).

Volume : faible (~10-50 actions sensibles/jour) — des ordres de grandeur sous stock_movement.


3.21 login_throttle

Throttle anti-brute-force par IP source. Complete le compteur par compte deja present sur user (failed_login_attempts / lockout_until), une ligne par IP source. Ajout security-by-design (voir note 13).

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
ip_address VARCHAR(45) NO UNIQUE IP source, une ligne par IP, upsertee ; 45 caracteres contiennent un litteral IPv6 complet
failed_attempts SMALLINT UNSIGNED NO 0 logins echoues consecutifs depuis cette IP dans la fenetre courante
window_started_at DATETIME NO CURRENT_TIMESTAMP debut de la fenetre de comptage courante
lockout_until DATETIME YES NULL fin de la fenetre de backoff degressif ; NULL = non throttle
last_attempt_at DATETIME NO CURRENT_TIMESTAMP timestamp de la derniere tentative echouee

Pas de FK : une IP n'est pas une entite modelisee. Les lignes sont appended/upsertees par IP ; la fenetre se reinitialise a son expiration. Un cron quotidien purge les lignes sans lockout actif dont le last_attempt_at est plus ancien que 24h.


3.22 pin_throttle

Throttle du PIN d'action sensible (RG-T22), complement de RG-T13. Une ligne par utilisateur AGISSANT (l'identite de session qui soumet email+PIN), STRICTEMENT SEPAREE des compteurs de connexion (user.failed_login_attempts / login_throttle) : un echec de PIN n'incremente aucun compteur de login. Ajout security-by-design (voir note 13).

Attribut Type NULL Default Contrainte Notes
id INT UNSIGNED NO AUTO_INCREMENT PK
actor_user_id INT UNSIGNED NO UNIQUE, FK -> user(id) ON DELETE CASCADE l'utilisateur agissant (session), une ligne par acteur, upsertee
failed_attempts SMALLINT UNSIGNED NO 0 echecs de PIN consecutifs de cet acteur dans la fenetre courante
window_started_at DATETIME NO CURRENT_TIMESTAMP debut de la fenetre de comptage courante
lockout_until DATETIME YES NULL fin de la fenetre de backoff degressif ; NULL = non throttle
last_attempt_at DATETIME NO CURRENT_TIMESTAMP timestamp de la derniere tentative echouee

FK ON DELETE CASCADE (contrairement a login_throttle) : la cle est un utilisateur back-office authentifie, donc supprimer/anonymiser le compte purge proprement sa ligne de throttle. Memes bornes de backoff que RG-8 mais PROPRES au PIN (PIN_THROTTLE_*, plus permissives). Meme purge cron quotidienne que login_throttle (lignes sans lockout actif > 24h).


4. 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 virgule flottante IEEE 754. Sommer 100 lignes de commande peut produire des ecarts au niveau du centime vs la realite metier.
  2. Conversion FLOAT-vers-chaine : differentes versions de driver 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 en 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.

Note 2 — Pourquoi ENUM plutot qu'une table de reference

Les ENUMs (service_mode, status, item_type, action, slot_type) auraient pu etre des tables de reference. Choix retenu : ENUM.

Avantages dans ce contexte :

  • Les valeurs sont stables et limitees (3-7 valeurs max), peu susceptibles d'evoluer frequemment.
  • Contrainte au niveau SGBD au lieu d'une FK a l'execution ; requetes plus simples.
  • Directement lisible en SQL : WHERE status = 'paid'.

Cout d'un changement futur : ALTER TABLE ... MODIFY COLUMN ... ENUM(...) pour ajouter une valeur. Acceptable etant donne que les changements sont attendus comme rares.

Si ces ENUMs requierent plus tard des libelles ou descriptions multilingues, ils seront migres vers des tables de reference. Hors perimetre pour cette iteration.

Note 3 — Pourquoi customer_order et non order

ORDER est un mot reserve SQL (utilise dans ORDER BY). Trois approches existent :

  • Quoter le nom partout : `order` — requiert un quoting dans chaque instruction SQL, source d'erreurs et non portable entre dialectes SGBD.
  • Utiliser un alias au niveau ORM : possible mais ajoute une couche de mapping.
  • Renommer : customer_order (choisi) — sans ambiguite, auto-documente, sans quoting requis.

Alternative consideree et rejetee : purchase (moins specifique au domaine), transaction (egalement reserve ou ambigu). customer_order correspond au langage du domaine et evite tous les conflits.

order_item est conserve comme nom de table de ligne : item n'est pas reserve, et le prefixe order_ rend claire la relation parent.

Note 4 — Prefixe de numero de commande par canal

Format : K/C/D-YYYY-MM-DD-NNN (kiosk / counter / drive).

Rationale : le prefixe encode le canal, ce qui est utile pour une identification visuelle rapide par le personnel cuisine et comptoir sans interroger la colonne source. Le compteur sequentiel NNN repart chaque jour par canal. Sans collision sur une journee, vu le volume attendu.

Alternative rejetee : prefixe neutre W- pour tous les canaux (plus simple, mais perd la lisibilite du canal pour le personnel).

Note 5 — source vs service_mode (canal vs mode de consommation)

Deux dimensions distinctes, gardees separees :

source service_mode
Nature canal de saisie (qui a saisi la commande) mode de consommation (ou le client mange)
Valeurs kiosk, counter, drive dine_in, takeaway, drive
Sert a authentification, analytics, filtrage de permission KPI, capacite (aucun role fiscal)

Les deux dimensions sont independantes pour kiosk et counter (un client borne peut choisir dine_in ou takeaway). drive est le seul cas ou les deux dimensions s'alignent : source=drive implique service_mode=drive. Cette contrainte croisee est verifiee au niveau applicatif.

Note 6 — Machine a 4 etats reduite

v0.1 avait 6 etats (pending_payment, paid, preparing, ready, delivered, cancelled). v0.2 reduit a 4 etats : pending_payment -> paid -> delivered (+ cancelled).

Rationale (Decision 4 de revue-alignement-p1.md §7) : dans un contexte fast-food, l'affichage cuisine (KDS) est un systeme visuel — le personnel voit le ticket et agit. preparing et ready etaient des etats intermediaires qui ajoutaient de la complexite sans valeur metier proportionnelle. L'unique action cuisine est deliver (le personnel counter/drive remet la commande), fusionnant preparing + ready + delivered en un seul geste. Le KPI est le temps total : delivered_at - paid_at (SLA ~10 min). Le codage couleur du KDS est calcule depuis now - paid_at, sans etat stocke supplementaire.

Etats et timestamps retires : preparing_at, ready_at ne sont pas stockes.

Note 7 — Cascade de format Normal / Maxi

Le format Maxi agrandit uniquement l'accompagnement et la boisson. Le burger est inchange et la portion de sauce est inchangee (un pot de sauce est identique dans les deux formats). Ce perimetre est explicite afin que le modele de stock reste fidele.

Cote prix — non modelise au niveau du prix de composant individuel :

  • menu porte deux prix : price_normal_cents et price_maxi_cents.
  • order_item.format enregistre le format choisi par le client (normal ou maxi).
  • order_item.unit_price_cents_snapshot capture le prix reellement paye (Normal ou Maxi).
  • Aucun prix individuel par composant de slot n'est stocke ; le differentiel de prix est un attribut au niveau menu, coherent avec la maniere dont les menus fast-food tendent a etre tarifes en pratique.

Cote stock — modelise via un multiplicateur de format sur la recette :

  • product_ingredient porte quantity_normal et quantity_maxi.
  • A la transition paid, le decrement utilise quantity_maxi quand order_item.format='maxi', sinon quantity_normal.
  • Pour les ingredients burger et sauce, quantity_maxi = quantity_normal (invariants au format).
  • Pour les ingredients accompagnement et boisson, quantity_maxi > quantity_normal (le Maxi consomme plus).
  • Le format se propage de la ligne de menu (order_item.format) a ses selections de slot ; une ligne de produit autonome est par defaut a normal.
  • Un seul produit par choix (ex. un produit Fries), pas de produits medium/large separes.

Calibration : le supplement Maxi est dans la fourchette ~1,50 EUR pour ce modele (derive de donnees internes ; recoupe avec l'ordre de grandeur du marche pour plausibilite. Wakdo est un pastiche fictif, donc les prix exacts ne sont pas copies d'une chaine reelle).

Calibration : le supplement Maxi est dans la fourchette ~1,50 EUR pour ce modele (derive de donnees internes ; recoupe avec l'ordre de grandeur du marche pour plausibilite. Wakdo est un pastiche fictif, donc les prix exacts ne sont pas copies d'une chaine reelle).

Note 8 — Stockage des images : chemin en VARCHAR vs BLOB en BDD

Les colonnes image_path (category, product, menu) stockent un chemin relatif depuis la racine publique (ex. /uploads/products/classic-burger.jpg), pas un chemin serveur absolu. PHP resout via un prefixe depuis .env (UPLOAD_DIR=public/uploads).

Le stockage BLOB a ete considere et rejete :

Critere image_path VARCHAR (choisi) BLOB en BDD
Performance borne Apache sert les fichiers en ms (cache OS) PHP lit la BDD + streame, latence multipliee
Cache HTTP ETag, Last-Modified, cache navigateur, CDN natifs doit etre reimplemente en PHP
Taille de backup BDD Megaoctets (chemins seulement) Gigaoctets (66 produits x ~200 Ko + variantes responsive)
Pipeline d'images convert, webp, optimisation = outils standard du systeme de fichiers doit etre reinvente en PHP

Sources : OWASP File Upload Cheat Sheet ; MariaDB Knowledge Base — LONGBLOB performance ; documentation Apache HTTP Server — serving static content.

Note 9 — Regle de TVA dans le fast-food francais (fact-checked)

FACT-CHECK
Claim audited : "TVA 10% sur place / 5,5% a emporter" (dictionary v0.1 note 9)
Domain         : compliance (fiscal)
Verdict        : CLAIM INEXACT — superseded
Source         : BOFiP BOI-ANNX-000495 + BOI-TVA-LIQ-30-10-10 (official doctrine impots.gouv.fr)
Actual rule    : 10% for immediate consumption (dine-in OR hot takeaway);
                 5.5% for products in resealable containers (bottle, can) / deferred consumption
Confidence     : 95% (L1, official text)

Consequence sur le modele : le taux de TVA est un attribut du product (vat_rate en pour-mille : 100 = 10%, 55 = 5,5%), pas de la commande ni du mode de service. Defaut : 100 (10%). Le taux de 5,5% s'applique aux produits en contenants refermables (eau en bouteille, bouteilles de jus). La TVA est calculee ligne par ligne ; le taux est snapshote sur order_item.vat_rate_snapshot au moment de la transaction pour preserver l'integrite historique si la legislation change.

service_mode est conserve sur customer_order pour les stats et le KPI uniquement (planification de capacite, repartition du chiffre d'affaires par mode). Il n'a aucun role de calcul fiscal.

Note 10 — Configurateur d'ingredients et rattachement du modificateur

order_item_modifier se rattache a une ligne order_item via order_item_id, que la ligne soit un produit autonome ou un menu.

Pour un produit autonome (item_type='product') : order_item_id identifie directement le produit modifie.

Pour un menu (item_type='menu') : le produit modifiable est le burger fixe, identifie via order_item.menu_id -> menu.burger_product_id. L'affichage cuisine resout : modifier.order_item_id -> order_item -> menu -> menu.burger_product_id -> product.name. Aucune colonne FK supplementaire n'est necessaire sur order_item_modifier. Cela garde la table modificateur simple et evite une colonne nullable target_product_id qui ne serait peuplee que pour les lignes de menu.

Contrainte appliquee au niveau applicatif : les lignes order_item_modifier pour une ligne de menu referencent uniquement des ingredients appartenant a menu.burger_product_id via product_ingredient.

Note 11 — Eligibilite menu_slot : filtre par categorie vs liste de produits explicite

Deux options ont ete considerees :

  • Filtre par categorie : menu_slot.category_id pointe vers une categorie ; tous les produits de cette categorie sont eligibles. Simple, mais une categorie peut contenir des produits non proposes dans ce slot (ex. une boisson premium ajoutee a la categorie "drinks" ne devrait pas apparaitre automatiquement dans tous les slots de menu).
  • Liste de produits explicite menu_slot_option(menu_slot_id, product_id) (choisie) : chaque produit eligible est liste explicitement par slot. Plus verbeux au moment du seed mais precis — pas d'eligibilite accidentelle quand le catalogue grandit. Permet des overrides de tarification par slot a l'avenir sans changement structurel.

La liste explicite ajoute une entite (menu_slot_option, entite 3.5) mais elimine une classe de bugs de justesse. Coherent avec l'ambition prod-like de ce modele.

Note 12 — commande_event retire

v0.1 portait une table d'audit append-only commande_event (pattern event sourcing). Retiree en v0.2 (Decision 1, revue-alignement-p1.md §7).

Rationale : dans un contexte restaurant, le compte back-office est partage par poste de travail, non individuel. L'attribution par personne d'une transition d'etat n'a aucune valeur metier. Le besoin reel (durees de phase, stats par heure de la journee) est couvert par les timestamps de phase sur customer_order (paid_at, delivered_at, cancelled_at) sans la complexite d'un event store.

La machine a 4 etats combinee a 3 timestamps de phase fournit toutes les donnees KPI necessaires :

  • Temps de remise : delivered_at - paid_at
  • Taux et timing d'annulation : cancelled_at - created_at
  • Volume par heure : calcul HOUR(created_at) / service_day

Pour l'audit de stock, stock_movement (entite 3.19) fournit la trace d'audit append-only la ou elle est genuinement necessaire (reconciliation d'inventaire).

Note 13 — Ajouts de donnees security-by-design (2026-06-11)

Ces ajouts etendent le modele prod-like avec une couche security-by-design. Ils ne remplacent aucune decision v0.2 ; ils ajoutent imputabilite, cycle de vie d'auth et resistance a l'abus.

Imputabilite — compte partage hybride + PIN. Les sessions back-office restent partagees par poste de travail pour le flux de routine (un terminal fast-food est partage, les equipiers tournent). Un PIN par membre du personnel (user.pin_hash, argon2id) autorise un ensemble defini d'actions sensibles (editions prix/menu 8.2/8.3/8.6, annulation de commande 7.1, correction d'inventaire 9.2, gestion des utilisateurs 10.1-10.3, RBAC 10.4). Ces actions ecrivent le user_id agissant dans audit_log (3.20). Cela resout la justification circulaire qui avait retire commande_event en v0.1 (les events etaient juges inutiles parce que les comptes etaient partages) : l'imputabilite est enregistree la ou elle importe, a friction quasi nulle pour les 95% de routine. customer_order.acting_user_id capture le personnel pour les commandes counter/drive prises sous PIN ; les commandes borne restent anonymes.

Cycle de vie d'auth. password_reset_token_hash + password_reset_expires_at permettent un parcours de reset (le token est stocke hashe, le token brut est envoye par e-mail une seule fois). La resistance au brute-force utilise un throttling degressif plutot qu'un verrouillage dur indefini : failed_login_attempts + lockout_until implementent un backoff degressif par (compte + IP source), de sorte qu'une serie de fautes de frappe ne verrouille pas toute une cuisine en plein service (15 h continues). Les logins echoues sont ecrits dans audit_log.

Anonymisation RGPD vs retention d'audit. Les PII de user (email, first_name, last_name) sont soumises au droit a l'effacement (Cr 3.d). L'effacement anonymise plutot qu'il ne supprime durement : la ligne est conservee, email devient un placeholder unique non identifiant (anon-<id>@wakdo.invalid, domaine reserve RFC 2606), les noms sont effaces, password_hash/pin_hash sont invalides, et anonymized_at est renseigne. audit_log conserve sa propre fenetre de retention (~12 mois, interet legitime / tracabilite fiscale) et continue de pointer vers le principal anonymise, de sorte que effacement et imputabilite coexistent sans casser l'integrite referentielle.

Resistance a l'abus sur la borne anonyme. customer_order.idempotency_key (UUID client, UNIQUE) deduplique un POST /api/orders reessaye de sorte qu'un retry reseau ne cree pas de commande payee dupliquee. Le stock est decremente avec une seule instruction atomique (UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id) : aucune operation ne depend d'une lecture de stock, donc la ligne s'auto-verrouille pour la duree de l'ecriture — pas de lost update et pas de souci d'ordre des deadlocks. Cela remplace l'approche pessimiste anterieure SELECT ... FOR UPDATE (regle de la couche traitement, voir mlt.md) ; elle n'ajoute aucune colonne ici.

Modele de stock en pourcentage + disponibilite calculee. ingredient porte stock_capacity (la reference 100%), low_stock_pct (bande dalerte) et critical_stock_pct (seuil de rupture automatique) — voir 3.6. stock_quantity est signe et peut devenir negatif (ampleur de survente remontee aux managers) ; le systeme ne bloque pas une commande sur le stock. La commandabilite effective du produit est calculee (regle RG-T21 dans mlt.md) : product.is_available = 1 ET chaque ingredient non retirable (is_removable=0) de son product_ingredient a stock_quantity > stock_capacity * critical_stock_pct/100. A la bande critique, un produit passe automatiquement en rupture sans ecriture ni cascade ; un retrait manuel (product.is_available=0) est une surcharge forte ; un reapprovisionnement au-dessus de la bande critique rend le produit a nouveau commandable de lui-meme.

Throttle anti-brute-force par IP. login_throttle (3.21) suit failed_attempts et lockout_until par IP source (une ligne upsertee par IP), completant le compteur par compte sur user. Cela ajoute une seconde dimension de throttling, de sorte qu'une seule IP martelant de nombreux comptes soit ralentie independamment du compteur de n'importe quel compte. Un cron quotidien purge les lignes inactives et non verrouillees.

Throttle du PIN d'action sensible (par acteur). pin_throttle (3.22) suit failed_attempts et lockout_until par utilisateur AGISSANT (l'identite de session qui valide une action sensible), dans une table separee des compteurs de connexion. La dimension est l'acteur (et non l'email cible, contournable par rotation, ni l'IP, qui penaliserait tous les equipiers d'un poste partage) ; le verrou est un backoff degressif aux bornes propres (PIN_THROTTLE_*). Meme purge cron que login_throttle. RG-T22.

References : docs/notes/revue-alignement-p1.md §7 (decisions D), carte d'impact security-by-design (2026-06-11). Modele de menace et matrice de classification des donnees : PROJECT_CONTEXT.md §19 (a venir).


5. Synthese du decompte des entites

# Entite Type Remplace / nouveau
1 category business v0.1 categorie (renommee + traduite)
2 product business v0.1 produit (+ vat_rate)
3 menu business v0.1 menu (+ FK burger, 2 prix)
4 menu_slot business nouveau — remplace la composition fixe menu_produit
5 menu_slot_option join nouveau — liste d'eligibilite par slot
6 ingredient business nouveau — configurateur d'ingredients + stock
7 product_ingredient join nouveau — recette + metadonnees de personnalisation
8 allergen reference nouveau — INCO 1169/2011
9 ingredient_allergen join nouveau — mappe les allergenes aux ingredients
10 customer_order business v0.1 commande (renommee, machine a 4 etats, timestamps de phase)
11 order_item business v0.1 ligne_commande (+ format, vat_rate_snapshot)
12 order_item_selection business nouveau — choix de slot de menu du client
13 order_item_modifier business nouveau — modifications au niveau ingredient
14 user business v0.1 user (noms de champs traduits)
15 role business v0.1 role (+ default_route, order_source)
16 role_visible_source join nouveau — filtre de tableau de bord par role
17 permission reference v0.1 permission (traduite, catalogue gele)
18 role_permission join v0.1 role_permission (inchangee)
19 stock_movement audit nouveau — journal d'audit de stock append-only
20 audit_log audit nouveau (security-by-design) — journal append-only d'actions sensibles
21 login_throttle security nouveau (security-by-design) - throttle anti-brute-force par IP
22 pin_throttle security nouveau (security-by-design) - throttle du PIN d'action sensible par acteur (RG-T22)

Retire de v0.1 : commande_event (remplace par les timestamps de phase sur customer_order), menu_produit (remplace par le modele menu_slot + menu_slot_option).

Total : 22 entites (19 prod-like v0.2 + audit_log, login_throttle et pin_throttle de la couche security-by-design).

Le security-by-design ajoute aussi des colonnes (au-dela des deux nouvelles entites) : cycle de vie d'auth de user + pin_hash + anonymized_at (3.14), customer_order.acting_user_id + idempotency_key (3.10), et le modele de stock en pourcentage sur ingredient (3.6) — stock_capacity, critical_stock_pct, plus le renommage de low_stock_threshold en low_stock_pct. login_throttle (3.21) est la 21e entite et pin_throttle (3.22) la 22e. Voir note 13.


Pour le diagramme ER et les justifications de cardinalite, voir mcd.md — le diagramme est la source de verite unique pour la representation graphique.