docs(merise): synchronise le referentiel avec la base deployee (5 migrations additives + order_number reel)

This commit is contained in:
Imugiii 2026-06-25 07:58:19 +00:00
parent 03ef99d67b
commit 10440bb266
12 changed files with 171 additions and 21 deletions

View file

@ -13,6 +13,9 @@ erDiagram
varchar name varchar name
text description text description
int price_cents int price_cents
int maxi_variant_product_id FK
smallint size_cl
int base_product_id FK
smallint vat_rate smallint vat_rate
varchar image_path varchar image_path
tinyint is_available tinyint is_available
@ -46,6 +49,8 @@ erDiagram
category ||--o{ product : "groups" category ||--o{ product : "groups"
category ||--o{ menu : "groups" category ||--o{ menu : "groups"
menu ||--|| product : "anchors (burger_product_id)" menu ||--|| product : "anchors (burger_product_id)"
product ||--o{ product : "maxi_variant (maxi_variant_product_id)"
product ||--o{ product : "size_variant_of (base_product_id)"
menu ||--o{ menu_slot : "defines_slot" menu ||--o{ menu_slot : "defines_slot"
menu_slot ||--o{ menu_slot_option : "lists" menu_slot ||--o{ menu_slot_option : "lists"
product ||--o{ menu_slot_option : "is_eligible_for" product ||--o{ menu_slot_option : "is_eligible_for"

View file

@ -11,6 +11,9 @@ erDiagram
int stock_capacity int stock_capacity
smallint pack_size smallint pack_size
varchar pack_label varchar pack_label
smallint energy_kcal_100g
varchar nutrition_source
datetime nutrition_fetched_at
smallint low_stock_pct smallint low_stock_pct
smallint critical_stock_pct smallint critical_stock_pct
tinyint is_active tinyint is_active

View file

@ -6,6 +6,7 @@ erDiagram
enum source enum source
int acting_user_id FK int acting_user_id FK
enum service_mode enum service_mode
varchar service_tag
enum status enum status
int total_ht_cents int total_ht_cents
int total_vat_cents int total_vat_cents

View file

@ -11,6 +11,9 @@ erDiagram
int category_id FK int category_id FK
varchar name varchar name
int price_cents int price_cents
int maxi_variant_product_id FK
smallint size_cl
int base_product_id FK
smallint vat_rate smallint vat_rate
tinyint is_available tinyint is_available
smallint display_order smallint display_order
@ -41,6 +44,8 @@ erDiagram
category ||--o{ product : "category_id (RESTRICT)" category ||--o{ product : "category_id (RESTRICT)"
category ||--o{ menu : "category_id (RESTRICT)" category ||--o{ menu : "category_id (RESTRICT)"
product ||--o{ menu : "burger_product_id (RESTRICT)" product ||--o{ menu : "burger_product_id (RESTRICT)"
product ||--o{ product : "maxi_variant_product_id (SET NULL)"
product ||--o{ product : "base_product_id (CASCADE)"
menu ||--o{ menu_slot : "menu_id (CASCADE)" menu ||--o{ menu_slot : "menu_id (CASCADE)"
menu_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)" menu_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)"
product ||--o{ menu_slot_option : "product_id (RESTRICT)" product ||--o{ menu_slot_option : "product_id (RESTRICT)"

View file

@ -6,6 +6,9 @@ erDiagram
int stock_quantity int stock_quantity
int stock_capacity int stock_capacity
smallint pack_size smallint pack_size
smallint energy_kcal_100g
varchar nutrition_source
datetime nutrition_fetched_at
smallint low_stock_pct smallint low_stock_pct
smallint critical_stock_pct smallint critical_stock_pct
tinyint is_active tinyint is_active

View file

@ -6,6 +6,7 @@ erDiagram
enum source enum source
int acting_user_id FK int acting_user_id FK
enum service_mode enum service_mode
varchar service_tag
enum status enum status
int total_ht_cents int total_ht_cents
int total_vat_cents int total_vat_cents

View file

@ -4,7 +4,7 @@
**Version** : v0.3 — prod-like, 22 entites (19 prod-like + couche security-by-design, incl. les entites `login_throttle` et `pin_throttle`) **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) **Date** : 2026-06-04 (ajouts security-by-design 2026-06-11)
**Branche** : `feat/p1-conception` **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) **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) ; colonnes additives post-v0.3 des migrations 0003/0005/0006/0007 alignees sur le deploye (voir note 14)
**Auteur** : BYAN (couche methodologie) **Auteur** : BYAN (couche methodologie)
--- ---
@ -114,6 +114,9 @@ Un article vendable unique, disponible a la carte ou comme composant dans un slo
| `name` | VARCHAR(120) | NO | — | INDEX | `nom` | renomme depuis `nom` | | `name` | VARCHAR(120) | NO | — | INDEX | `nom` | renomme depuis `nom` |
| `description` | TEXT | YES | NULL | — | (ajoute) | renseigne plus tard via l'admin | | `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) | | `price_cents` | INT UNSIGNED | NO | — | CHECK > 0 | `prix` (FLOAT) | conversion FLOAT -> INT centimes au seed (voir note 1) |
| `maxi_variant_product_id` | INT UNSIGNED | YES | NULL | FK -> `product(id)`, ON DELETE SET NULL | (migration 0006) | auto-reference : variante servie quand un menu est commande au format Maxi (ex. Moyenne Frite -> Grande Frite). Data-driven (la regle vit dans la donnee). SET NULL = degradation gracieuse : si la variante Grande est retiree du catalogue, le produit de base reste vendable, il perd seulement sa substitution Maxi. Voir note 14 |
| `size_cl` | SMALLINT UNSIGNED | YES | NULL | — | (migration 0007) | variante de TAILLE a la carte : volume en centilitres d'une boisson fontaine (ex. 30 / 50 cl). NULL = produit sans dimension taille (bouteille, non-boisson). La ligne de base ET la variante portent leur volume pour l'affichage du picker. Voir note 14 |
| `base_product_id` | INT UNSIGNED | YES | NULL | FK -> `product(id)`, ON DELETE CASCADE | (migration 0007) | auto-reference vers la ligne de base d'une variante de taille. NULL = produit de base ou autonome (visible dans la grille catalogue) ; NON NULL = variante de taille (masquee de la grille, atteinte via le picker). CASCADE : une variante de taille n'a pas de sens sans sa base (suppression de la base -> suppression de ses variantes). Voir note 14 |
| `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 | | `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 | | `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 | | `is_available` | TINYINT(1) | NO | 1 | — | (ajoute) | bascule de disponibilite manuelle depuis l'admin |
@ -197,6 +200,9 @@ Ingredient elementaire utilise dans la composition des produits. Porte les donne
| `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 | | `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_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") | | `pack_label` | VARCHAR(80) | YES | NULL | — | libelle humain du pack (ex. "Sac 100 portions") |
| `energy_kcal_100g` | SMALLINT UNSIGNED | YES | NULL | — | enrichissement nutritionnel (migration 0005) : apport energetique pour 100 g, importe depuis l'API externe OpenFoodFacts (Cr 3.a.3). Nullable : un ingredient non enrichi reste valide. Voir note 14 |
| `nutrition_source` | VARCHAR(120) | YES | NULL | — | enrichissement nutritionnel (migration 0005) : provenance de la donnee (ex. "OpenFoodFacts"). Voir note 14 |
| `nutrition_fetched_at` | DATETIME | YES | NULL | — | enrichissement nutritionnel (migration 0005) : horodatage de l'import, pour tracer la fraicheur. Voir note 14 |
| `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 | | `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 | | `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 | | `is_active` | TINYINT(1) | NO | 1 | — | desactiver les ingredients obsoletes sans supprimer |
@ -281,11 +287,12 @@ Transaction client : 1 commande = 1 panier valide a un instant donne.
| Attribut | Type | NULL | Default | Contrainte | Notes | | Attribut | Type | NULL | Default | Contrainte | Notes |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | | | `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. | | `order_number` | VARCHAR(20) | NO | — | UNIQUE | numero lisible : prefixe canal + id sequentiel, soit `K<id>` / `C<id>` / `D<id>` (K=kiosk, C=counter, D=drive). Ecrit en deux temps (INSERT puis UPDATE avec `LAST_INSERT_ID()`). 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 | | `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. | | `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 | | `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). | | `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). |
| `service_tag` | VARCHAR(20) | YES | NULL | — | numero de chevalet pour le service EN SALLE (migration 0003), saisi a la borne quand le client choisit `dine_in` ; permet d'apporter la commande a la bonne table (B4). NULL pour `takeaway` / `drive`. Voir note 14 |
| `status` | ENUM('pending_payment','paid','delivered','cancelled') | NO | 'pending_payment' | INDEX | machine a 4 etats : `pending_payment -> paid -> delivered` (+ `cancelled`). Voir note 6. | | `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_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_vat_cents` | INT UNSIGNED | NO | — | CHECK >= 0 | montant de TVA, snapshot |
@ -687,15 +694,28 @@ et evite tous les conflits.
`order_item` est conserve comme nom de table de ligne : `item` n'est pas reserve, et le `order_item` est conserve comme nom de table de ligne : `item` n'est pas reserve, et le
prefixe `order_` rend claire la relation parent. prefixe `order_` rend claire la relation parent.
### Note 4 — Prefixe de numero de commande par canal ### Note 4 — Prefixe de numero de commande par canal (existant : prefixe + id)
Format : `K`/`C`/`D`-YYYY-MM-DD-NNN (kiosk / counter / drive). Format reel (decision utilisateur) : prefixe canal + id de la commande, soit `K<id>` / `C<id>` /
`D<id>` (kiosk / counter / drive). Implemente dans `src/app/Order/OrderRepository.php` : la commande
est inseree avec un `order_number` provisoire vide, puis l'`order_number` est ecrit en `prefix .
LAST_INSERT_ID()` (ex. `K42`, `C7`, `D13`).
Rationale : le prefixe encode le canal, ce qui est utile pour une identification visuelle rapide Rationale : le prefixe encode le canal, utile pour une identification visuelle rapide par le personnel
par le personnel cuisine et comptoir sans interroger la colonne `source`. Le compteur sequentiel NNN cuisine et comptoir sans interroger la colonne `source`. Le suffixe est l'id sequentiel auto-incremente :
repart chaque jour par canal. Sans collision sur une journee, vu le volume attendu. pas de compteur quotidien a tenir ni de `service_day` a gerer cote numerotation (rasoir d'Ockham,
mantra #37).
Alternative rejetee : prefixe neutre `W-` pour tous les canaux (plus simple, mais perd la lisibilite Ecart assume avec la cible v0.x initiale `K-AAAA-MM-JJ-NNN` (compteur journalier par canal) :
cette derniere n'a pas ete retenue a l'implementation, jugee plus lourde sans valeur metier
proportionnelle pour le volume attendu. La forme acte ici est celle qui tourne.
Compromis connu : un numero `prefixe + id` est sequentiel, donc devinable (un client peut incrementer
l'id). Couple a l'endpoint de paiement anonyme cote borne (lecture/encaissement par `order_number`
sans authentification), c'est une surface a surveiller. Piste d'amelioration : numero non sequentiel
(ex. suffixe aleatoire court) si le suivi anonyme par numero devait s'ouvrir davantage.
Alternative rejetee : prefixe neutre `W` pour tous les canaux (plus simple, mais perd la lisibilite
du canal pour le personnel). du canal pour le personnel).
### Note 5 — `source` vs `service_mode` (canal vs mode de consommation) ### Note 5 — `source` vs `service_mode` (canal vs mode de consommation)
@ -910,6 +930,49 @@ est un backoff degressif aux bornes propres (PIN_THROTTLE_*). Meme purge cron qu
References : `docs/notes/revue-alignement-p1.md` §7 (decisions D), carte d'impact security-by-design 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). (2026-06-11). Modele de menace et matrice de classification des donnees : `PROJECT_CONTEXT.md` §19 (a venir).
### Note 14 — Colonnes additives post-v0.3 (migrations 0003 / 0005 / 0006 / 0007)
Ces colonnes etendent le schema apres v0.3 par des migrations purement additives (ajout de colonnes
nullables et de FK auto-referentes ; aucune donnee existante a retro-remplir, aucune table nouvelle).
Le runner applique les `*.sql` dans l'ordre lexicographique via `schema_migrations`. Elles sont alignees
ici sur le schema reellement deploye.
**Migration 0003 — `customer_order.service_tag` VARCHAR(20) NULL (AFTER service_mode).** Numero de
chevalet pour le service en salle (mode `dine_in`), saisi a la borne ; NULL pour `takeaway` / `drive`.
Permet d'apporter la commande a la bonne table (B4). Entite 3.10.
**Migration 0005 — enrichissement nutritionnel de `ingredient` (AFTER pack_label).**
`energy_kcal_100g` SMALLINT UNSIGNED NULL, `nutrition_source` VARCHAR(120) NULL,
`nutrition_fetched_at` DATETIME NULL. Donnees importees depuis l'API externe OpenFoodFacts (Cr 3.a.3 :
exploitation d'informations externes dans le modele de donnees). Opt-in et egress maitrise : aucun appel
automatique au runtime borne ; la passerelle (`App\Catalogue\OpenFoodFactsGateway`) est invoquee seulement
par `IngredientController::enrich` (action explicite manager/admin). Toutes nullables : un ingredient non
enrichi reste valide. Entite 3.6.
**Migration 0006 — `product.maxi_variant_product_id` INT UNSIGNED NULL, FK -> `product(id)` ON DELETE
SET NULL (AFTER price_cents).** Auto-reference : variante servie quand un menu est commande au format
Maxi (ex. Moyenne Frite -> Grande Frite), substituee cote serveur dans `OrderRepository::resolveSelections`
sans choix supplementaire. Approche data-driven (la regle vit dans la donnee, pas dans le code), et le
decrement de stock frappe alors le bon produit. SET NULL plutot que RESTRICT : si la variante Grande est
supprimee du catalogue, le produit de base reste vendable et perd seulement sa substitution Maxi
(degradation gracieuse) ; la reference est un confort metier, pas une integrite forte de commande (les
commandes figent deja leurs snapshots). Entite 3.2.
**Migration 0007 — variante de TAILLE de `product` (AFTER price_cents).** `size_cl` SMALLINT UNSIGNED
NULL, `base_product_id` INT UNSIGNED NULL avec FK -> `product(id)` ON DELETE CASCADE. La dimension taille
des boissons fontaine (la maquette borne propose 30 / 50 cl) est modelisee en lignes produit distinctes
(meme approche que Moyenne/Grande Frite) : le domaine commande facture deja par `product_id`, le flux reste
inchange, la borne resout la taille choisie en `product_id`. `base_product_id` NULL = produit de base ou
autonome (visible dans la grille catalogue) ; NON NULL = variante de taille (masquee de la grille, atteinte
via le picker). CASCADE plutot que SET NULL (a la difference de 0006) : une variante de taille n'a aucun
sens sans sa base (une "Coca Cola 50 cl" orpheline n'est pas commercialisable), donc supprimer la base
emporte ses variantes de taille. Les deux groupings coexistent sur une boisson sans se confondre :
`base_product_id` pilote la selection de taille a la carte (picker 30/50 cl) ; `maxi_variant_product_id`
(0006) pilote la substitution Maxi en menu. Entite 3.2.
References : `db/migrations/0003_order_service_tag.sql`, `0005_ingredient_nutrition.sql`,
`0006_product_maxi_variant.sql`, `0007_product_size_variant.sql`.
--- ---
## 5. Synthese du decompte des entites ## 5. Synthese du decompte des entites

View file

@ -111,6 +111,9 @@ erDiagram
varchar name varchar name
text description text description
int price_cents int price_cents
int maxi_variant_product_id FK
smallint size_cl
int base_product_id FK
smallint vat_rate smallint vat_rate
varchar image_path varchar image_path
tinyint is_available tinyint is_available
@ -144,6 +147,8 @@ erDiagram
category ||--o{ product : "groups" category ||--o{ product : "groups"
category ||--o{ menu : "groups" category ||--o{ menu : "groups"
menu ||--|| product : "anchors (burger_product_id)" menu ||--|| product : "anchors (burger_product_id)"
product ||--o{ product : "maxi_variant (maxi_variant_product_id)"
product ||--o{ product : "size_variant_of (base_product_id)"
menu ||--o{ menu_slot : "defines_slot" menu ||--o{ menu_slot : "defines_slot"
menu_slot ||--o{ menu_slot_option : "lists" menu_slot ||--o{ menu_slot_option : "lists"
product ||--o{ menu_slot_option : "is_eligible_for" product ||--o{ menu_slot_option : "is_eligible_for"
@ -159,6 +164,8 @@ erDiagram
| C4 | defines_slot | menu | (1,N) | menu_slot | (1,1) | Un menu doit definir au moins un slot (boisson, accompagnement, sauce) pour avoir une composition personnalisable. Un slot appartient a exactement un menu. | | C4 | defines_slot | menu | (1,N) | menu_slot | (1,1) | Un menu doit definir au moins un slot (boisson, accompagnement, sauce) pour avoir une composition personnalisable. Un slot appartient a exactement un menu. |
| C5 | lists | menu_slot | (1,N) | menu_slot_option | (1,1) | Un slot doit lister au moins un produit eligible (sinon le client ne peut pas le remplir). Chaque ligne d'option appartient a exactement un slot. | | C5 | lists | menu_slot | (1,N) | menu_slot_option | (1,1) | Un slot doit lister au moins un produit eligible (sinon le client ne peut pas le remplir). Chaque ligne d'option appartient a exactement un slot. |
| C6 | is_eligible_for | product | (0,N) | menu_slot_option | (1,1) | Un produit peut etre eligible pour un nombre quelconque de slots a travers tous les menus, ou aucun s'il n'est vendu qu'a la carte. Chaque ligne d'option reference exactement un produit. | | C6 | is_eligible_for | product | (0,N) | menu_slot_option | (1,1) | Un produit peut etre eligible pour un nombre quelconque de slots a travers tous les menus, ou aucun s'il n'est vendu qu'a la carte. Chaque ligne d'option reference exactement un produit. |
| C7 | maxi_variant (migration 0006) | product (base) | (0,1) | product (variante Maxi) | (0,N) | Auto-reference : un produit pointe vers 0 ou 1 variante servie en menu Maxi (`maxi_variant_product_id`, nullable) ; un produit peut etre la variante Maxi de plusieurs autres. ON DELETE SET NULL (degradation gracieuse). |
| C8 | size_variant_of (migration 0007) | product (base) | (0,N) | product (variante de taille) | (0,1) | Auto-reference : une variante de taille pointe vers sa ligne de base (`base_product_id`, nullable ; NULL = base/autonome) ; un produit de base peut avoir plusieurs variantes de taille (30/50 cl). ON DELETE CASCADE (une variante de taille n'a pas de sens sans sa base). |
### 4.3 Notes sur le sous-domaine Catalogue ### 4.3 Notes sur le sous-domaine Catalogue
@ -168,6 +175,8 @@ erDiagram
**Format Normal / Maxi** : deux prix (`price_normal_cents`, `price_maxi_cents`) sur `menu` ; format enregistre au niveau de `order_item.format`. Aucun differentiel de prix au niveau du slot individuel n'est stocke (voir note 7 du dictionnaire). **Format Normal / Maxi** : deux prix (`price_normal_cents`, `price_maxi_cents`) sur `menu` ; format enregistre au niveau de `order_item.format`. Aucun differentiel de prix au niveau du slot individuel n'est stocke (voir note 7 du dictionnaire).
**Variantes de produit (migrations 0006 / 0007, voir note 14 du dictionnaire)** : deux auto-references sur `product`, distinctes par leur role et leur comportement `ON DELETE`. `maxi_variant_product_id` (SET NULL) designe la variante servie quand un menu est commande au format Maxi (ex. Moyenne Frite -> Grande Frite), substituee cote serveur. `size_cl` + `base_product_id` (CASCADE) modelisent une variante de TAILLE a la carte (boisson 30/50 cl) en lignes produit : `base_product_id` NULL = produit de base/autonome visible au catalogue, NON NULL = variante masquee de la grille et atteinte via le picker. Les deux groupings coexistent sur une boisson sans se confondre.
--- ---
## 5. Sous-domaine : Ingredients & Stock ## 5. Sous-domaine : Ingredients & Stock
@ -188,6 +197,9 @@ erDiagram
int stock_capacity int stock_capacity
smallint pack_size smallint pack_size
varchar pack_label varchar pack_label
smallint energy_kcal_100g
varchar nutrition_source
datetime nutrition_fetched_at
smallint low_stock_pct smallint low_stock_pct
smallint critical_stock_pct smallint critical_stock_pct
tinyint is_active tinyint is_active
@ -262,6 +274,8 @@ erDiagram
**Disponibilite produit calculee (regle RG-T21, voir `mlt.md`)** : la commandabilite effective est derivee, pas stockee. Un produit est commandable quand `product.is_available = 1` ET que chaque ingredient non retirable (`is_removable = 0`) de son `product_ingredient` a `stock_quantity > stock_capacity * critical_stock_pct / 100`. Un ingredient requis atteignant la bande critique met le produit en rupture automatique sans ecriture et sans cascade ; un retrait manuel (`product.is_available = 0`) est une surcharge forte ; un reapprovisionnement au-dessus de la bande critique rend le produit commandable a nouveau de lui-meme. Un ingredient retirable/optionnel a la bande critique ne bloque pas le produit (seul son supplement devient indisponible). Le tableau de bord distingue un retrait manuel d'une rupture pilotee par le stock. **Disponibilite produit calculee (regle RG-T21, voir `mlt.md`)** : la commandabilite effective est derivee, pas stockee. Un produit est commandable quand `product.is_available = 1` ET que chaque ingredient non retirable (`is_removable = 0`) de son `product_ingredient` a `stock_quantity > stock_capacity * critical_stock_pct / 100`. Un ingredient requis atteignant la bande critique met le produit en rupture automatique sans ecriture et sans cascade ; un retrait manuel (`product.is_available = 0`) est une surcharge forte ; un reapprovisionnement au-dessus de la bande critique rend le produit commandable a nouveau de lui-meme. Un ingredient retirable/optionnel a la bande critique ne bloque pas le produit (seul son supplement devient indisponible). Le tableau de bord distingue un retrait manuel d'une rupture pilotee par le stock.
**Enrichissement nutritionnel (migration 0005, voir note 14 du dictionnaire)** : `energy_kcal_100g`, `nutrition_source` et `nutrition_fetched_at` (toutes nullables) stockent une donnee importee depuis l'API externe OpenFoodFacts (Cr 3.a.3 : exploitation d'informations externes dans le modele de donnees). Opt-in : l'import est declenche par `IngredientController::enrich` (action manager/admin), pas au runtime borne ; un ingredient non enrichi reste valide.
--- ---
## 6. Sous-domaine : Order ## 6. Sous-domaine : Order
@ -277,6 +291,7 @@ erDiagram
enum source enum source
int acting_user_id FK int acting_user_id FK
enum service_mode enum service_mode
varchar service_tag
enum status enum status
int total_ht_cents int total_ht_cents
int total_vat_cents int total_vat_cents
@ -382,6 +397,11 @@ enregistre l'employe de comptoir/drive qui a pris la commande sous PIN ; NULL po
Cela ajoute une association `customer_order |o--o| user : "taken_by"` (cardinalite : une commande est Cela ajoute une association `customer_order |o--o| user : "taken_by"` (cardinalite : une commande est
prise par (0,1) user ; un user prend (0,N) commandes). Voir note 13 du dictionnaire. prise par (0,1) user ; un user prend (0,N) commandes). Voir note 13 du dictionnaire.
**Numero de commande (existant) et service en salle (migration 0003)** : `order_number` est un attribut
non cle (UNIQUE) de forme `prefixe canal + id` (`K<id>` / `C<id>` / `D<id>`) ; pas une association. Voir
note 4 du dictionnaire. `service_tag` (VARCHAR(20), nullable) porte le numero de chevalet du service en
salle (mode `dine_in`), saisi a la borne ; NULL pour `takeaway` / `drive`. Voir note 14 du dictionnaire.
--- ---
## 7. Sous-domaine : RBAC ## 7. Sous-domaine : RBAC
@ -580,7 +600,7 @@ Le MCD reste au niveau conceptuel. Les decisions suivantes sont reportees au MLD
des PK composites. des PK composites.
2. **PK technique vs identifiant metier** : `id INT UNSIGNED AUTO_INCREMENT` sur toutes les entites principales. 2. **PK technique vs identifiant metier** : `id INT UNSIGNED AUTO_INCREMENT` sur toutes les entites principales.
`customer_order` porte en plus `order_number VARCHAR(20) UNIQUE` (lisible par un humain, `customer_order` porte en plus `order_number VARCHAR(20) UNIQUE` (lisible par un humain,
format `K/C/D-YYYY-MM-DD-NNN` par canal). format prefixe canal + id : `K<id>` / `C<id>` / `D<id>`).
3. **Regles ON DELETE** : CASCADE vs RESTRICT vs SET NULL. Detaillees dans le MLD. 3. **Regles ON DELETE** : CASCADE vs RESTRICT vs SET NULL. Detaillees dans le MLD.
4. **Contraintes CHECK** : exclusivite de polymorphisme sur `order_item`, contrainte croisee 4. **Contraintes CHECK** : exclusivite de polymorphisme sur `order_item`, contrainte croisee
`source/service_mode` sur `customer_order`, invariant arithmetique sur les totaux. `source/service_mode` sur `customer_order`, invariant arithmetique sur les totaux.

View file

@ -143,7 +143,7 @@ Pour chaque operation, le document fournit :
| **Synchronisation** | AND (les deux actions requises) | | **Synchronisation** | AND (les deux actions requises) |
| **Condition** | Le panier contient au moins 1 article. Le numero de commande saisi est non vide. | | **Condition** | Le panier contient au moins 1 article. Le numero de commande saisi est non vide. |
| **Operation** | CREATE_ORDER | | **Operation** | CREATE_ORDER |
| **Description** | Creation atomique de la commande : INSERT `customer_order` avec statut `pending_payment`, source `kiosk`, snapshot des totaux HT/TVA/TTC (calcules ligne par ligne en utilisant `vat_rate` snapshote par article). INSERT des lignes `order_item` avec `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`. INSERT `order_item_selection` pour chaque slot rempli dans un article de menu. INSERT `order_item_modifier` pour chaque modification d'ingredient. Decrement de `ingredient.stock_quantity` pour chaque ingredient consomme (ajuste par les modificateurs : retrait => pas de decrement ; ajout => decrement supplementaire) ; INSERT d'une ligne `stock_movement` de type `sale` par unite d'ingredient affectee. Les decrements de stock et l'insertion de la commande sont dans la meme transaction. Apres que le client a saisi son numero de commande, le statut passe `pending_payment -> paid` dans la meme transaction ; `paid_at` est positionne. Le systeme genere le numero de commande au format `K-YYYY-MM-DD-NNN`. | | **Description** | Creation atomique de la commande : INSERT `customer_order` avec statut `pending_payment`, source `kiosk`, snapshot des totaux HT/TVA/TTC (calcules ligne par ligne en utilisant `vat_rate` snapshote par article). INSERT des lignes `order_item` avec `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`. INSERT `order_item_selection` pour chaque slot rempli dans un article de menu. INSERT `order_item_modifier` pour chaque modification d'ingredient. Decrement de `ingredient.stock_quantity` pour chaque ingredient consomme (ajuste par les modificateurs : retrait => pas de decrement ; ajout => decrement supplementaire) ; INSERT d'une ligne `stock_movement` de type `sale` par unite d'ingredient affectee. Les decrements de stock et l'insertion de la commande sont dans la meme transaction. Apres que le client a saisi son numero de commande, le statut passe `pending_payment -> paid` dans la meme transaction ; `paid_at` est positionne. Le systeme genere le numero de commande au format prefixe canal + id (`K<id>`, voir dictionnaire note 4). |
| **Entites MCD** | R: `product`, `menu`, `ingredient`, `product_ingredient` (snapshot) — W: `customer_order` (INSERT status `pending_payment` then UPDATE status `paid`, `paid_at`), `order_item` (INSERT N lines), `order_item_selection` (INSERT per menu slot chosen), `order_item_modifier` (INSERT per modification), `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `sale` per unit) | | **Entites MCD** | R: `product`, `menu`, `ingredient`, `product_ingredient` (snapshot) — W: `customer_order` (INSERT status `pending_payment` then UPDATE status `paid`, `paid_at`), `order_item` (INSERT N lines), `order_item_selection` (INSERT per menu slot chosen), `order_item_modifier` (INSERT per modification), `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `sale` per unit) |
| **Resultat** | Commande creee (statut `paid` en fin d'operation), numero de commande affiche au client, evenement logique ORDER_CREATED emis vers le domaine de preparation | | **Resultat** | Commande creee (statut `paid` en fin d'operation), numero de commande affiche au client, evenement logique ORDER_CREATED emis vers le domaine de preparation |
@ -175,7 +175,7 @@ Pour chaque operation, le document fournit :
| **Synchronisation** | Aucune | | **Synchronisation** | Aucune |
| **Condition** | L'acteur est authentifie et detient la permission `order.create`. La `source` est `counter` ou `drive` (auto-taggee depuis `role.order_source`). | | **Condition** | L'acteur est authentifie et detient la permission `order.create`. La `source` est `counter` ou `drive` (auto-taggee depuis `role.order_source`). |
| **Operation** | CREATE_COUNTER_ORDER | | **Operation** | CREATE_COUNTER_ORDER |
| **Description** | Composition manuelle de la commande via le back-office : selectionner produits et menus, choisir le mode de service (`dine_in`/`takeaway`/`drive`), remplir les slots de menu, ajouter des modificateurs d'ingredient. Logique de creation identique a CREATE_ORDER (snapshot, decrement de stock dans la meme transaction, transition atomique `pending_payment -> paid`). La `source` est auto-taggee depuis `role.order_source` (counter -> `counter`, drive -> `drive`). Format du numero de commande : `C-YYYY-MM-DD-NNN` (comptoir) ou `D-YYYY-MM-DD-NNN` (drive). Contrainte croisee : si `source = 'drive'` alors `service_mode = 'drive'` (verifie a la creation). | | **Description** | Composition manuelle de la commande via le back-office : selectionner produits et menus, choisir le mode de service (`dine_in`/`takeaway`/`drive`), remplir les slots de menu, ajouter des modificateurs d'ingredient. Logique de creation identique a CREATE_ORDER (snapshot, decrement de stock dans la meme transaction, transition atomique `pending_payment -> paid`). La `source` est auto-taggee depuis `role.order_source` (counter -> `counter`, drive -> `drive`). Format du numero de commande : prefixe canal + id (`C<id>` comptoir, `D<id>` drive). Contrainte croisee : si `source = 'drive'` alors `service_mode = 'drive'` (verifie a la creation). |
| **Entites MCD** | R: `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `product_ingredient` — W: `customer_order` (INSERT status `pending_payment` then UPDATE status `paid`, `paid_at`), `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient` (stock decrement), `stock_movement` (INSERT type `sale`) | | **Entites MCD** | R: `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `product_ingredient` — W: `customer_order` (INSERT status `pending_payment` then UPDATE status `paid`, `paid_at`), `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient` (stock decrement), `stock_movement` (INSERT type `sale`) |
| **Resultat** | Commande creee (statut `paid`), numero de commande communique au client | | **Resultat** | Commande creee (statut `paid`), numero de commande communique au client |

View file

@ -125,6 +125,9 @@ erDiagram
int category_id FK int category_id FK
varchar name varchar name
int price_cents int price_cents
int maxi_variant_product_id FK
smallint size_cl
int base_product_id FK
smallint vat_rate smallint vat_rate
tinyint is_available tinyint is_available
smallint display_order smallint display_order
@ -155,6 +158,8 @@ erDiagram
category ||--o{ product : "category_id (RESTRICT)" category ||--o{ product : "category_id (RESTRICT)"
category ||--o{ menu : "category_id (RESTRICT)" category ||--o{ menu : "category_id (RESTRICT)"
product ||--o{ menu : "burger_product_id (RESTRICT)" product ||--o{ menu : "burger_product_id (RESTRICT)"
product ||--o{ product : "maxi_variant_product_id (SET NULL)"
product ||--o{ product : "base_product_id (CASCADE)"
menu ||--o{ menu_slot : "menu_id (CASCADE)" menu ||--o{ menu_slot : "menu_id (CASCADE)"
menu_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)" menu_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)"
product ||--o{ menu_slot_option : "product_id (RESTRICT)" product ||--o{ menu_slot_option : "product_id (RESTRICT)"
@ -171,6 +176,9 @@ erDiagram
int stock_quantity int stock_quantity
int stock_capacity int stock_capacity
smallint pack_size smallint pack_size
smallint energy_kcal_100g
varchar nutrition_source
datetime nutrition_fetched_at
smallint low_stock_pct smallint low_stock_pct
smallint critical_stock_pct smallint critical_stock_pct
tinyint is_active tinyint is_active
@ -235,6 +243,7 @@ erDiagram
enum source enum source
int acting_user_id FK int acting_user_id FK
enum service_mode enum service_mode
varchar service_tag
enum status enum status
int total_ht_cents int total_ht_cents
int total_vat_cents int total_vat_cents
@ -410,11 +419,14 @@ Pas de FK. Table racine du sous-domaine Catalogue.
### 4.2 `product` ### 4.2 `product`
``` ```
product (id, #category_id, name, [description], price_cents, vat_rate, product (id, #category_id, name, [description], price_cents,
[#maxi_variant_product_id], [size_cl], [#base_product_id], vat_rate,
[image_path], is_available, display_order, created_at, updated_at) [image_path], is_available, display_order, created_at, updated_at)
PK : id PK : id
FK : category_id -> category(id) ON DELETE RESTRICT FK : category_id -> category(id) ON DELETE RESTRICT
FK : maxi_variant_product_id -> product(id) ON DELETE SET NULL
FK : base_product_id -> product(id) ON DELETE CASCADE
IDX : (category_id, is_available, display_order) IDX : (category_id, is_available, display_order)
CHK : price_cents > 0 CHK : price_cents > 0
CHK : vat_rate IN (55, 100) CHK : vat_rate IN (55, 100)
@ -427,6 +439,9 @@ product (id, #category_id, name, [description], price_cents, vat_rate,
| `name` | VARCHAR(120) | NO | Libelle du produit | | `name` | VARCHAR(120) | NO | Libelle du produit |
| `description` | TEXT | YES | Description longue optionnelle | | `description` | TEXT | YES | Description longue optionnelle |
| `price_cents` | INT UNSIGNED | NO | Prix a la carte, TVA incluse, en centimes | | `price_cents` | INT UNSIGNED | NO | Prix a la carte, TVA incluse, en centimes |
| `maxi_variant_product_id` | INT UNSIGNED | YES | FK -> product (auto-reference), ON DELETE SET NULL ; variante servie en menu Maxi (migration 0006, voir note de table) |
| `size_cl` | SMALLINT UNSIGNED | YES | Volume en cl d'une variante de taille de boisson ; NULL si pas de dimension taille (migration 0007) |
| `base_product_id` | INT UNSIGNED | YES | FK -> product (auto-reference), ON DELETE CASCADE ; ligne de base d'une variante de taille, NULL = base/autonome (migration 0007) |
| `vat_rate` | SMALLINT UNSIGNED | NO | Pour-mille : 100 = 10%, 55 = 5.5% | | `vat_rate` | SMALLINT UNSIGNED | NO | Pour-mille : 100 = 10%, 55 = 5.5% |
| `image_path` | VARCHAR(255) | YES | Chemin relatif depuis la racine publique | | `image_path` | VARCHAR(255) | YES | Chemin relatif depuis la racine publique |
| `is_available` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Bascule de disponibilite manuelle | | `is_available` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Bascule de disponibilite manuelle |
@ -437,6 +452,19 @@ product (id, #category_id, name, [description], price_cents, vat_rate,
**ON DELETE RESTRICT** sur `category_id` : une categorie avec des produits ne peut pas etre supprimee. Empeche les **ON DELETE RESTRICT** sur `category_id` : une categorie avec des produits ne peut pas etre supprimee. Empeche les
produits orphelins. produits orphelins.
**Auto-references de variante (migrations 0006 / 0007, voir note 14 du dictionnaire)** : deux groupings
distincts, tous deux pointant vers `product(id)`.
- `maxi_variant_product_id` (ON DELETE SET NULL) : variante servie quand un menu est commande au format
Maxi (ex. Moyenne Frite -> Grande Frite). SET NULL = degradation gracieuse, le produit de base reste
vendable si la variante Grande est supprimee.
- `size_cl` + `base_product_id` (ON DELETE CASCADE) : variante de TAILLE a la carte (boisson 30/50 cl)
modelisee en lignes produit. `base_product_id` NULL = produit de base/autonome (visible catalogue) ;
NON NULL = variante de taille (masquee de la grille, atteinte via le picker). CASCADE car une variante
de taille n'a pas de sens sans sa base.
Les deux coexistent sur une boisson sans se confondre : `base_product_id` pilote la selection de taille a
la carte ; `maxi_variant_product_id` pilote la substitution Maxi en menu.
--- ---
### 4.3 `menu` ### 4.3 `menu`
@ -529,6 +557,7 @@ Pas d'horodatages. Table de jointure pure.
``` ```
ingredient (id, name, unit, stock_quantity, stock_capacity, pack_size, [pack_label], ingredient (id, name, unit, stock_quantity, stock_capacity, pack_size, [pack_label],
[energy_kcal_100g], [nutrition_source], [nutrition_fetched_at],
low_stock_pct, critical_stock_pct, is_active, created_at, updated_at) low_stock_pct, critical_stock_pct, is_active, created_at, updated_at)
PK : id PK : id
@ -549,6 +578,9 @@ ingredient (id, name, unit, stock_quantity, stock_capacity, pack_size, [pack_lab
| `stock_capacity` | INT NOT NULL | NO | Niveau "plein" de reference en unites = le 100% utilise pour calculer le pourcentage de stock ; CHECK > 0 protege aussi la division du pourcentage contre la division par zero | | `stock_capacity` | INT NOT NULL | NO | Niveau "plein" de reference en unites = le 100% utilise pour calculer le pourcentage de stock ; CHECK > 0 protege aussi la division du pourcentage contre la division par zero |
| `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Unites par lot de reapprovisionnement | | `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Unites par lot de reapprovisionnement |
| `pack_label` | VARCHAR(80) | YES | Libelle humain du lot | | `pack_label` | VARCHAR(80) | YES | Libelle humain du lot |
| `energy_kcal_100g` | SMALLINT UNSIGNED | YES | Apport energetique pour 100 g, importe depuis l'API externe OpenFoodFacts (migration 0005) |
| `nutrition_source` | VARCHAR(120) | YES | Provenance de la donnee nutritionnelle, ex. "OpenFoodFacts" (migration 0005) |
| `nutrition_fetched_at` | DATETIME | YES | Horodatage de l'import nutritionnel, trace la fraicheur (migration 0005) |
| `low_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 10 | NO | Bande dalerte, pourcentage de la capacite (CHECK BETWEEN 0 AND 100) | | `low_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 10 | NO | Bande dalerte, pourcentage de la capacite (CHECK BETWEEN 0 AND 100) |
| `critical_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 5 | NO | Plancher de rupture automatique, pourcentage de la capacite (CHECK BETWEEN 0 AND 100 ; CHECK de table `critical_stock_pct < low_stock_pct`) | | `critical_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 5 | NO | Plancher de rupture automatique, pourcentage de la capacite (CHECK BETWEEN 0 AND 100 ; CHECK de table `critical_stock_pct < low_stock_pct`) |
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Desactiver les ingredients obsoletes | | `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Desactiver les ingredients obsoletes |
@ -577,6 +609,11 @@ bloque pas le produit (seul son supplement devient indisponible). Le tableau de
retrait manuel (`is_available=0`) d'une rupture pilotee par le stock (`is_available=1` mais un ingredient retrait manuel (`is_available=0`) d'une rupture pilotee par le stock (`is_available=1` mais un ingredient
requis est critique). requis est critique).
**Enrichissement nutritionnel (migration 0005, voir note 14 du dictionnaire)** : `energy_kcal_100g`,
`nutrition_source` et `nutrition_fetched_at` (toutes nullables) stockent une donnee importee depuis l'API
externe OpenFoodFacts (Cr 3.a.3). Opt-in : l'import est declenche par `IngredientController::enrich`
(action manager/admin), pas au runtime borne ; un ingredient non enrichi reste valide.
--- ---
### 4.7 `product_ingredient` ### 4.7 `product_ingredient`
@ -811,7 +848,7 @@ Pas d'horodatages. Table de jointure pure.
``` ```
customer_order (id, order_number, [idempotency_key], source, [#acting_user_id], customer_order (id, order_number, [idempotency_key], source, [#acting_user_id],
service_mode, status, service_mode, [service_tag], status,
total_ht_cents, total_vat_cents, total_ttc_cents, total_ht_cents, total_vat_cents, total_ttc_cents,
[paid_at], [delivered_at], [cancelled_at], [paid_at], [delivered_at], [cancelled_at],
created_at, updated_at) created_at, updated_at)
@ -833,11 +870,12 @@ customer_order (id, order_number, [idempotency_key], source, [#acting_user_id],
| Colonne | Type | NULL | Notes | | Colonne | Type | NULL | Notes |
|---|---|---|---| |---|---|---|---|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK | | `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
| `order_number` | VARCHAR(20) | NO | Format `K/C/D-YYYY-MM-DD-NNN` par canal | | `order_number` | VARCHAR(20) | NO | Prefixe canal + id sequentiel : `K<id>`/`C<id>`/`D<id>` (existant, voir note de table) |
| `idempotency_key` | VARCHAR(36) | YES | UUID client, UNIQUE ; deduplique un POST reessaye (security-by-design) | | `idempotency_key` | VARCHAR(36) | YES | UUID client, UNIQUE ; deduplique un POST reessaye (security-by-design) |
| `source` | ENUM('kiosk','counter','drive') | NO | Canal de saisie | | `source` | ENUM('kiosk','counter','drive') | NO | Canal de saisie |
| `acting_user_id` | INT UNSIGNED | YES | FK -> user ; personnel counter/drive sous PIN ; NULL pour kiosk | | `acting_user_id` | INT UNSIGNED | YES | FK -> user ; personnel counter/drive sous PIN ; NULL pour kiosk |
| `service_mode` | ENUM('dine_in','takeaway','drive') | NO | Mode de consommation (stats uniquement, pas de role fiscal) | | `service_mode` | ENUM('dine_in','takeaway','drive') | NO | Mode de consommation (stats uniquement, pas de role fiscal) |
| `service_tag` | VARCHAR(20) | YES | Numero de chevalet du service en salle (`dine_in`), saisi a la borne ; NULL pour takeaway/drive (migration 0003) |
| `status` | ENUM('pending_payment','paid','delivered','cancelled') NOT NULL DEFAULT 'pending_payment' | NO | Machine a 4 etats | | `status` | ENUM('pending_payment','paid','delivered','cancelled') NOT NULL DEFAULT 'pending_payment' | NO | Machine a 4 etats |
| `total_ht_cents` | INT UNSIGNED | NO | Snapshot du total HT | | `total_ht_cents` | INT UNSIGNED | NO | Snapshot du total HT |
| `total_vat_cents` | INT UNSIGNED | NO | Snapshot du montant de TVA | | `total_vat_cents` | INT UNSIGNED | NO | Snapshot du montant de TVA |
@ -848,6 +886,17 @@ customer_order (id, order_number, [idempotency_key], source, [#acting_user_id],
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Utilise comme base de `service_day` | | `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Utilise comme base de `service_day` |
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit | | `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
**Numero de commande (existant, voir note 4 du dictionnaire)** : `order_number` = prefixe canal + id
sequentiel (`K<id>` / `C<id>` / `D<id>`), ecrit en deux temps (INSERT avec numero provisoire vide, puis
UPDATE en `prefix . LAST_INSERT_ID()`) dans `OrderRepository`. La cible initiale `K-AAAA-MM-JJ-NNN`
(compteur journalier) n'a pas ete retenue : la forme `prefixe + id` evite un compteur quotidien. Compromis
connu : numero sequentiel donc devinable, couple a l'endpoint de paiement anonyme cote borne (piste
d'amelioration : numero non sequentiel).
**Service en salle (migration 0003)** : `service_tag` (VARCHAR(20), nullable) porte le numero de chevalet
saisi a la borne pour le mode `dine_in` ; NULL pour `takeaway` / `drive`. Colonne additive, sans contrainte
de BD (la coherence avec `service_mode` est appliquee au niveau applicatif).
**Attribution du personnel (security-by-design)** : `acting_user_id` (FK -> `user`, ON DELETE SET NULL) **Attribution du personnel (security-by-design)** : `acting_user_id` (FK -> `user`, ON DELETE SET NULL)
enregistre le personnel counter/drive qui a pris la commande sous PIN ; NULL pour les commandes kiosk anonymes. enregistre le personnel counter/drive qui a pris la commande sous PIN ; NULL pour les commandes kiosk anonymes.
Les commandes kiosk restent anonymes par conception. `stock_movement.user_id` couvre l'attribution des actions Les commandes kiosk restent anonymes par conception. `stock_movement.user_id` couvre l'attribution des actions
@ -1235,11 +1284,11 @@ et que toutes les tables se rattachent au MCD.
| Entite MCD | Table MLD | Type de mapping | Notes | | Entite MCD | Table MLD | Type de mapping | Notes |
|---|---|---|---| |---|---|---|---|
| `category` (C1) | `category` (4.1) | entite 1:1 | | | `category` (C1) | `category` (4.1) | entite 1:1 | |
| `product` (C2) | `product` (4.2) | entite 1:1 | | | `product` (C2) | `product` (4.2) | entite 1:1 | Additif post-v0.3 : `maxi_variant_product_id` (0006), `size_cl` + `base_product_id` (0007) |
| `menu` (C3) | `menu` (4.3) | entite 1:1 | Nouveau : `burger_product_id`, `price_normal_cents`, `price_maxi_cents` | | `menu` (C3) | `menu` (4.3) | entite 1:1 | Nouveau : `burger_product_id`, `price_normal_cents`, `price_maxi_cents` |
| `menu_slot` (C4) | `menu_slot` (4.4) | entite 1:1 | Nouvelle entite (v0.2) | | `menu_slot` (C4) | `menu_slot` (4.4) | entite 1:1 | Nouvelle entite (v0.2) |
| `menu_slot_option` (C5) | `menu_slot_option` (4.5) | Table de jointure (PK composite) | Nouvelle entite (v0.2) | | `menu_slot_option` (C5) | `menu_slot_option` (4.5) | Table de jointure (PK composite) | Nouvelle entite (v0.2) |
| `ingredient` (C6) | `ingredient` (4.6) | entite 1:1 | Nouvelle entite (v0.2) | | `ingredient` (C6) | `ingredient` (4.6) | entite 1:1 | Nouvelle entite (v0.2) ; additif post-v0.3 : `energy_kcal_100g`, `nutrition_source`, `nutrition_fetched_at` (0005) |
| `product_ingredient` (C7) | `product_ingredient` (4.7) | Table de jointure avec attributs | Nouvelle entite (v0.2) | | `product_ingredient` (C7) | `product_ingredient` (4.7) | Table de jointure avec attributs | Nouvelle entite (v0.2) |
| `allergen` (C8) | `allergen` (4.8) | entite 1:1 | Nouvelle entite (v0.2) | | `allergen` (C8) | `allergen` (4.8) | entite 1:1 | Nouvelle entite (v0.2) |
| `ingredient_allergen` (C9) | `ingredient_allergen` (4.9) | Table de jointure (PK composite) | Nouvelle entite (v0.2) | | `ingredient_allergen` (C9) | `ingredient_allergen` (4.9) | Table de jointure (PK composite) | Nouvelle entite (v0.2) |
@ -1248,7 +1297,7 @@ et que toutes les tables se rattachent au MCD.
| `role_visible_source` (C12) | `role_visible_source` (4.12) | Table de jointure (PK composite) | Nouvelle entite (v0.2) | | `role_visible_source` (C12) | `role_visible_source` (4.12) | Table de jointure (PK composite) | Nouvelle entite (v0.2) |
| `permission` (C13) | `permission` (4.13) | entite 1:1 | | | `permission` (C13) | `permission` (4.13) | entite 1:1 | |
| `role_permission` (C14) | `role_permission` (4.14) | Table de jointure (PK composite) | | | `role_permission` (C14) | `role_permission` (4.14) | Table de jointure (PK composite) | |
| `customer_order` (C15) | `customer_order` (4.15) | entite 1:1 | Renommee depuis `commande` ; machine a 4 etats ; horodatages de phase | | `customer_order` (C15) | `customer_order` (4.15) | entite 1:1 | Renommee depuis `commande` ; machine a 4 etats ; horodatages de phase ; additif post-v0.3 : `service_tag` (0003) |
| `order_item` (C16) | `order_item` (4.16) | entite 1:1 | Nouveau : `format`, `vat_rate_snapshot` ; CHECK de polymorphisme | | `order_item` (C16) | `order_item` (4.16) | entite 1:1 | Nouveau : `format`, `vat_rate_snapshot` ; CHECK de polymorphisme |
| `order_item_selection` (C17) | `order_item_selection` (4.17) | entite 1:1 | Nouvelle entite (v0.2) | | `order_item_selection` (C17) | `order_item_selection` (4.17) | entite 1:1 | Nouvelle entite (v0.2) |
| `order_item_modifier` (C18) | `order_item_modifier` (4.18) | entite 1:1 | Nouvelle entite (v0.2) | | `order_item_modifier` (C18) | `order_item_modifier` (4.18) | entite 1:1 | Nouvelle entite (v0.2) |

View file

@ -115,7 +115,7 @@ Ces regles s'appliquent a plusieurs operations et sont centralisees ici pour evi
| **[PRE-3]** | Le corps JSON du POST est valide (validation de schema a la couche API) | | **[PRE-3]** | Le corps JSON du POST est valide (validation de schema a la couche API) |
| **[RG-1]** | Verification de disponibilite cote serveur : pour chaque article, verifier `product.is_available = 1` ou `menu.is_available = 1`. Si un article est indisponible, rejeter avec la liste des articles indisponibles. | | **[RG-1]** | Verification de disponibilite cote serveur : pour chaque article, verifier `product.is_available = 1` ou `menu.is_available = 1`. Si un article est indisponible, rejeter avec la liste des articles indisponibles. |
| **[RG-2 — service_day]** | Le `service_day` d'une commande donnee est calcule a l'execution de la requete comme : `CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END`. La coupure est a 10:00. Ce n'est PAS stocke comme colonne — calcule uniquement a l'execution de la requete. La formule v0.1 avec `INTERVAL 4 HOUR 30 MINUTE` etait incorrecte et est abandonnee. | | **[RG-2 — service_day]** | Le `service_day` d'une commande donnee est calcule a l'execution de la requete comme : `CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END`. La coupure est a 10:00. Ce n'est PAS stocke comme colonne — calcule uniquement a l'execution de la requete. La formule v0.1 avec `INTERVAL 4 HOUR 30 MINUTE` etait incorrecte et est abandonnee. |
| **[RG-3 — order number]** | Format du numero de commande : `K-YYYY-MM-DD-NNN` ou NNN est le compteur sequentiel pour le service_day courant pour la source `kiosk` (SELECT COUNT + 1 avec un verrou au niveau table ou un insert serialise pour eviter une generation en double sous concurrence). La source est `kiosk` (definie par l'endpoint kiosk, derivee du point d'entree public). | | **[RG-3 — order number]** | Format du numero de commande : prefixe canal + id auto-incremente, soit `K<id>` pour la source `kiosk` (ex. `K42`). Genere en deux temps dans la transaction : INSERT avec `order_number` provisoire vide, puis UPDATE `prefix . LAST_INSERT_ID()`. Pas de compteur par service_day (voir dictionnaire note 4). La source est `kiosk` (derivee de l'endpoint public). Le numero provisoire vide partage avant l'UPDATE reste une surface de robustesse a durcir sous forte concurrence (suivi au backlog). |
| **[RG-4 — VAT by line]** | Pour chaque `order_item` : `vat_rate_snapshot` est copie depuis `product.vat_rate`. Montants de ligne : `unit_ttc = unit_price_cents_snapshot` ; `unit_ht = ROUND(unit_ttc * 1000 / (1000 + vat_rate_snapshot))` ; `unit_vat = unit_ttc - unit_ht`. Totaux de commande : `total_ttc_cents = SUM(unit_ttc * quantity)` sur toutes les lignes ; `total_ht_cents = SUM(unit_ht * quantity)` ; `total_vat_cents = total_ttc_cents - total_ht_cents`. Invariant : `total_ttc_cents = total_ht_cents + total_vat_cents` (verifie avant l'INSERT). | | **[RG-4 — VAT by line]** | Pour chaque `order_item` : `vat_rate_snapshot` est copie depuis `product.vat_rate`. Montants de ligne : `unit_ttc = unit_price_cents_snapshot` ; `unit_ht = ROUND(unit_ttc * 1000 / (1000 + vat_rate_snapshot))` ; `unit_vat = unit_ttc - unit_ht`. Totaux de commande : `total_ttc_cents = SUM(unit_ttc * quantity)` sur toutes les lignes ; `total_ht_cents = SUM(unit_ht * quantity)` ; `total_vat_cents = total_ttc_cents - total_ht_cents`. Invariant : `total_ttc_cents = total_ht_cents + total_vat_cents` (verifie avant l'INSERT). |
| **[RG-5 — atomic transaction]** | Toutes les ecritures dans une seule transaction de base de donnees : (1) INSERT `customer_order` (status `pending_payment`, source `kiosk`, service_mode depuis le panier, totaux calcules) ; (2) INSERT des lignes `order_item` (label_snapshot, unit_price_cents_snapshot, vat_rate_snapshot, quantity, format, item_type, product_id ou menu_id) ; (3) INSERT des lignes `order_item_selection` pour chaque slot rempli dans un article menu (order_item_id, menu_slot_id, product_id, label_snapshot) ; (4) INSERT des lignes `order_item_modifier` pour chaque modification d'ingredient (order_item_id, ingredient_id, action, extra_price_cents snapshot) ; (5) pour chaque ingredient consomme : calculer units = `(order_item.format = 'maxi' ? product_ingredient.quantity_maxi : product_ingredient.quantity_normal) * order_item.quantity`, ajuste par les modificateurs (remove => pas de decrement pour cet ingredient ; add => decrement supplementaire) ; appliquer le decrement atomique `UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id` (instruction unique auto-verrouillante, sans lecture-gate prealable, RG-T20) ; `stock_quantity` est signe et peut devenir negatif (ampleur de survente, remontee aux managers) — le decrement ne se conditionne pas a un plancher ; INSERT `stock_movement` (type `sale`, delta = -units, order_id, user_id = NULL pour le kiosk) ; (6) UPDATE `customer_order` SET status = `paid`, `paid_at = NOW()`. Les six etapes committent ensemble ou sont entierement annulees. | | **[RG-5 — atomic transaction]** | Toutes les ecritures dans une seule transaction de base de donnees : (1) INSERT `customer_order` (status `pending_payment`, source `kiosk`, service_mode depuis le panier, totaux calcules) ; (2) INSERT des lignes `order_item` (label_snapshot, unit_price_cents_snapshot, vat_rate_snapshot, quantity, format, item_type, product_id ou menu_id) ; (3) INSERT des lignes `order_item_selection` pour chaque slot rempli dans un article menu (order_item_id, menu_slot_id, product_id, label_snapshot) ; (4) INSERT des lignes `order_item_modifier` pour chaque modification d'ingredient (order_item_id, ingredient_id, action, extra_price_cents snapshot) ; (5) pour chaque ingredient consomme : calculer units = `(order_item.format = 'maxi' ? product_ingredient.quantity_maxi : product_ingredient.quantity_normal) * order_item.quantity`, ajuste par les modificateurs (remove => pas de decrement pour cet ingredient ; add => decrement supplementaire) ; appliquer le decrement atomique `UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id` (instruction unique auto-verrouillante, sans lecture-gate prealable, RG-T20) ; `stock_quantity` est signe et peut devenir negatif (ampleur de survente, remontee aux managers) — le decrement ne se conditionne pas a un plancher ; INSERT `stock_movement` (type `sale`, delta = -units, order_id, user_id = NULL pour le kiosk) ; (6) UPDATE `customer_order` SET status = `paid`, `paid_at = NOW()`. Les six etapes committent ensemble ou sont entierement annulees. |
| **[RG-6 — cross-constraint]** | La source `kiosk` n'implique aucune contrainte particuliere de service_mode ; le client selectionne `dine_in` ou `takeaway`. La contrainte croisee drive (RG-T09) ne s'applique pas aux commandes provenant du kiosk. | | **[RG-6 — cross-constraint]** | La source `kiosk` n'implique aucune contrainte particuliere de service_mode ; le client selectionne `dine_in` ou `takeaway`. La contrainte croisee drive (RG-T09) ne s'applique pas aux commandes provenant du kiosk. |
@ -163,7 +163,7 @@ Ces regles s'appliquent a plusieurs operations et sont centralisees ici pour evi
| **[PRE-3]** | Le panier contient au moins 1 article | | **[PRE-3]** | Le panier contient au moins 1 article |
| **[RG-1]** | Logique de creation identique a CREATE_ORDER (RG-1 a RG-7 s'appliquent), avec les differences suivantes : `source` est auto-tagguee depuis `role.order_source` (role comptoir -> `counter`, role drive -> `drive`) ; `service_mode` est selectionne par le membre du personnel (`dine_in` / `takeaway` / `drive`) ; `user_id` est defini a l'id de l'utilisateur authentifie dans les lignes `stock_movement` (au lieu de NULL pour le kiosk). | | **[RG-1]** | Logique de creation identique a CREATE_ORDER (RG-1 a RG-7 s'appliquent), avec les differences suivantes : `source` est auto-tagguee depuis `role.order_source` (role comptoir -> `counter`, role drive -> `drive`) ; `service_mode` est selectionne par le membre du personnel (`dine_in` / `takeaway` / `drive`) ; `user_id` est defini a l'id de l'utilisateur authentifie dans les lignes `stock_movement` (au lieu de NULL pour le kiosk). |
| **[RG-2 — cross-constraint]** | Si `source = 'drive'` alors `service_mode` doit etre `'drive'` (RG-T09) ; verifie avant l'INSERT. HTTP 422 si viole. | | **[RG-2 — cross-constraint]** | Si `source = 'drive'` alors `service_mode` doit etre `'drive'` (RG-T09) ; verifie avant l'INSERT. HTTP 422 si viole. |
| **[RG-3 — order number]** | Format : `C-YYYY-MM-DD-NNN` pour la source comptoir ; `D-YYYY-MM-DD-NNN` pour la source drive. Le compteur sequentiel NNN est par source par service_day. | | **[RG-3 — order number]** | Format : prefixe canal + id auto-incremente, soit `C<id>` (comptoir) ou `D<id>` (drive). Meme generation en deux temps que CREATE_ORDER RG-3. Pas de compteur par service_day (voir dictionnaire note 4). |
| **[RG-4 — stock]** | Meme logique de decrement de stock que CREATE_ORDER RG-5 ; `stock_movement.user_id` est defini a l'id du membre du personnel authentifie. | | **[RG-4 — stock]** | Meme logique de decrement de stock que CREATE_ORDER RG-5 ; `stock_movement.user_id` est defini a l'id du membre du personnel authentifie. |
| **[RG-5 — staff attribution + decrement]** | `customer_order.acting_user_id` est defini a l'id du membre du personnel authentifie (imputabilite ciblee sur les commandes comptoir/drive ; les commandes kiosk restent NULL). La re-validation des modificateurs cote serveur (3.3 RG-9), l'idempotence (RG-T19) et le decrement de stock atomique (RG-T20) s'appliquent a l'identique. Aucun PIN n'est requis pour creer une commande (la permission `order.create` suffit) ; la creation de commande n'est pas dans l'ensemble des actions sensibles. | | **[RG-5 — staff attribution + decrement]** | `customer_order.acting_user_id` est defini a l'id du membre du personnel authentifie (imputabilite ciblee sur les commandes comptoir/drive ; les commandes kiosk restent NULL). La re-validation des modificateurs cote serveur (3.3 RG-9), l'idempotence (RG-T19) et le decrement de stock atomique (RG-T20) s'appliquent a l'identique. Aucun PIN n'est requis pour creer une commande (la permission `order.create` suffit) ; la creation de commande n'est pas dans l'ensemble des actions sensibles. |
| **[POST-1]** | Une ligne `customer_order` avec `status = 'paid'`, `source = 'counter'` ou `'drive'`, `paid_at` defini, `acting_user_id` defini. | | **[POST-1]** | Une ligne `customer_order` avec `status = 'paid'`, `source = 'counter'` ou `'drive'`, `paid_at` defini, `acting_user_id` defini. |

View file

@ -211,7 +211,7 @@ flowchart LR
| Cas | Operation MCT | Permission | Description | Entites | | Cas | Operation MCT | Permission | Description | Entites |
|---|---|---|---|---| |---|---|---|---|---|
| Saisir une commande comptoir/drive | 4.1 CREATE_COUNTER_ORDER | `order.create` | Composer une commande pour un client au comptoir (`counter`) ou au drive (`drive`). Logique identique a CREATE_ORDER ; `source` auto-tague depuis `role.order_source`. Numero `C-`/`D-YYYY-MM-DD-NNN`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient`, `stock_movement` | | Saisir une commande comptoir/drive | 4.1 CREATE_COUNTER_ORDER | `order.create` | Composer une commande pour un client au comptoir (`counter`) ou au drive (`drive`). Logique identique a CREATE_ORDER ; `source` auto-tague depuis `role.order_source`. Numero prefixe canal + id (`C<id>`/`D<id>`, voir dictionnaire note 4). | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient`, `stock_movement` |
| Consulter la file de preparation | 5.1 LIST_ORDERS_DISPLAY | `order.read` | Voir les commandes `paid` triees par `paid_at` croissant, filtrees par `role_visible_source` (counter voit kiosk+counter ; drive voit drive). Couleur KDS = `now - paid_at`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` | | Consulter la file de preparation | 5.1 LIST_ORDERS_DISPLAY | `order.read` | Voir les commandes `paid` triees par `paid_at` croissant, filtrees par `role_visible_source` (counter voit kiosk+counter ; drive voit drive). Couleur KDS = `now - paid_at`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` |
| Remettre la commande | 6.1 DELIVER_ORDER | `order.deliver` | Geste unique `paid -> delivered`, `delivered_at = NOW()`. | `customer_order` | | Remettre la commande | 6.1 DELIVER_ORDER | `order.deliver` | Geste unique `paid -> delivered`, `delivered_at = NOW()`. | `customer_order` |
| Annuler une commande | 7.1 CANCEL_ORDER | `order.cancel` | Transition vers `cancelled` depuis `pending_payment`/`paid`, `cancelled_at = NOW()`. Re-credit du stock si `paid`. | `customer_order`, `ingredient`, `stock_movement` | | Annuler une commande | 7.1 CANCEL_ORDER | `order.cancel` | Transition vers `cancelled` depuis `pending_payment`/`paid`, `cancelled_at = NOW()`. Re-credit du stock si `paid`. | `customer_order`, `ingredient`, `stock_movement` |