docs(merise): translate prose to French (project ASCII convention), code identifiers unchanged
All checks were successful
CI / secret-scan (pull_request) Successful in 16s
CI / php-lint (pull_request) Successful in 29s
CI / static-tests (pull_request) Successful in 9s

This commit is contained in:
Imugiii 2026-06-15 12:58:01 +00:00
parent 79d8ad9985
commit aa6b386f3d
5 changed files with 2063 additions and 2063 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,97 +1,97 @@
# Conceptual Data Model (MCD) — Wakdo
# Modele Conceptuel de Donnees (MCD) — Wakdo
**Merise phase** : P1 - Conception, step 2 (data dictionary first, mantra #33)
**Version** : v0.2 — prod-like, 21 entities (19 prod-like + security-by-design layer)
**Date** : 2026-06-04 (security-by-design additions 2026-06-11)
**Branch** : `feat/p1-conception`
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7); security-by-design layer (audit_log + accountability/auth columns) in progress
**Author** : BYAN (methodology layer)
**Phase Merise** : P1 - Conception, etape 2 (data dictionary first, mantra #33)
**Version** : v0.2 — prod-like, 21 entites (19 prod-like + couche security-by-design)
**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 (audit_log + colonnes imputabilite/auth) en cours
**Auteur** : BYAN (couche methodologie)
---
## 1. Purpose of this document
## 1. Objectif de ce document
The MCD (Modele Conceptuel des Donnees) formalises the **entities** of the Wakdo domain,
their **associations**, and the **cardinalities** governing those associations.
It is the normalised translation of the data dictionary, and serves as the basis for the
MLD (relational mapping).
Le MCD (Modele Conceptuel des Donnees) formalise les **entites** du domaine Wakdo,
leurs **associations**, et les **cardinalites** qui regissent ces associations.
C'est la traduction normalisee du dictionnaire de donnees, et il sert de base au
MLD (mapping relationnel).
Unlike the dictionary (which details attributes and types), the MCD focuses on relational
structure: how many X per Y, whether participation is mandatory, whether associations carry
their own attributes.
Contrairement au dictionnaire (qui detaille les attributs et les types), le MCD se concentre sur la
structure relationnelle : combien de X par Y, si la participation est obligatoire, si les associations portent
leurs propres attributs.
**Sources**:
- `docs/merise/dictionary.md` (v0.2 — 21 entities, source of truth for all names, types, ENUMs)
- `docs/notes/revue-alignement-p1.md` §7 (decision table D1-D8 + stock)
- `docs/PROJECT_CONTEXT.md` (business rules: menu composition, order flow, RBAC, service modes)
- `docs/merise/_sources/` (school data: 9 categories, 53 products, 13 menus)
**Sources** :
- `docs/merise/dictionary.md` (v0.2 — 21 entites, source de verite pour tous les noms, types, ENUMs)
- `docs/notes/revue-alignement-p1.md` §7 (table de decisions D1-D8 + stock)
- `docs/PROJECT_CONTEXT.md` (regles metier : composition de menu, flux de commande, RBAC, modes de service)
- `docs/merise/_sources/` (donnees de l'ecole : 9 categories, 53 produits, 13 menus)
---
## 2. Merise notation used
## 2. Notation Merise utilisee
### Cardinalities at the association foot (French Merise style)
### Cardinalites au pied de l'association (style Merise francais)
At each end of an association, the cardinality `(min,max)` states how many times an
instance of the entity participates in the association.
A chaque extremite d'une association, la cardinalite `(min,max)` indique combien de fois une
instance de l'entite participe a l'association.
```
ENTITY_A (min,max) ----[ ASSOCIATION ]---- (min,max) ENTITY_B
```
| Notation | Reading | Example |
| Notation | Lecture | Exemple |
|---|---|---|
| `(0,1)` | Optional, at most 1 | A stock_movement links to (0,1) customer_order |
| `(1,1)` | Mandatory, exactly 1 | A product belongs to (1,1) category |
| `(0,N)` | Optional, unbounded | A category groups (0,N) products |
| `(1,N)` | At least 1, unbounded | An order contains (1,N) order_items |
| `(0,1)` | Optionnel, au plus 1 | Un stock_movement est lie a (0,1) customer_order |
| `(1,1)` | Obligatoire, exactement 1 | Un product appartient a (1,1) category |
| `(0,N)` | Optionnel, non borne | Une category regroupe (0,N) products |
| `(1,N)` | Au moins 1, non borne | Une commande contient (1,N) order_items |
Reading: "one instance of the source entity participates at least MIN times and at most
MAX times in the association".
Lecture : "une instance de l'entite source participe au moins MIN fois et au plus
MAX fois a l'association".
### Association naming convention
### Convention de nommage des associations
Active verb in business terms, e.g.: `groups`, `anchors`, `defines_slot`, `contains`,
Verbe d'action en termes metier, par exemple : `groups`, `anchors`, `defines_slot`, `contains`,
`references_product`, `references_menu`, `fills_slot`, `modifies_ingredient`, `logs`,
`holds`, `grants`, `filters_source`, `decrements`.
N-N associations that carry their own attributes become **associative entities** in the MLD
(join table with own columns).
Les associations N-N qui portent leurs propres attributs deviennent des **entites associatives** dans le MLD
(table de jointure avec colonnes propres).
---
## 3. Decomposition by sub-domain
## 3. Decomposition par sous-domaine
The 21-entity model is split into 4 sub-domains for readability. Beyond approximately
5 entities, a single flat diagram becomes difficult to read; decomposition is the standard
Merise practice for models of this size.
Le modele de 21 entites est divise en 4 sous-domaines pour la lisibilite. Au-dela d'environ
5 entites, un diagramme plat unique devient difficile a lire ; la decomposition est la pratique
Merise standard pour les modeles de cette taille.
| Sub-domain | Entities | Count |
| Sous-domaine | Entites | Nombre |
|---|---|---|
| Catalogue | category, product, menu, menu_slot, menu_slot_option | 5 |
| Ingredients & Stock | ingredient, product_ingredient, allergen, ingredient_allergen, stock_movement | 5 |
| Order | customer_order, order_item, order_item_selection, order_item_modifier | 4 |
| RBAC & Audit | user, role, role_visible_source, permission, role_permission, audit_log, login_throttle | 7 |
> **Security-by-design layer (2026-06-11)**: `audit_log` (entity 20) is a cross-cutting,
> append-only log of sensitive actions; it is placed in the RBAC & Audit sub-domain because
> its references (`actor_user_id`, `actor_role_id`) are RBAC entities. `login_throttle`
> (entity 21) is a per-source-IP brute-force throttle, keyed by IP and carrying no FK; it sits
> in the same sub-domain because it guards the authentication path. New columns on existing
> entities: `user` auth-lifecycle + `pin_hash` + `anonymized_at`, `customer_order.acting_user_id`
> + `idempotency_key`. See dictionary note 13.
> **Couche security-by-design (2026-06-11)** : `audit_log` (entite 20) est un journal transverse,
> append-only des actions sensibles ; il est place dans le sous-domaine RBAC & Audit parce que
> ses references (`actor_user_id`, `actor_role_id`) sont des entites RBAC. `login_throttle`
> (entite 21) est un throttle anti-brute-force par IP source, indexe par IP et ne portant aucune FK ; il se situe
> dans le meme sous-domaine parce qu'il protege le chemin d'authentification. Nouvelles colonnes sur des entites existantes :
> `user` cycle de vie auth + `pin_hash` + `anonymized_at`, `customer_order.acting_user_id`
> + `idempotency_key`. Voir note 13 du dictionnaire.
**Note on the absence of a global diagram**: a single 21-entity ER diagram would be
unreadable and unmaintainable. The sub-domain decomposition below is the intentional
structural choice. Each sub-domain is a Mermaid `erDiagram` (authoritative, rendered
natively) with a portable SVG render in `docs/merise/_diagrams/`; see section 11 for the
sources and regeneration command.
**Note sur l'absence d'un diagramme global** : un unique diagramme ER de 21 entites serait
illisible et impossible a maintenir. La decomposition par sous-domaine ci-dessous est le choix
structurel intentionnel. Chaque sous-domaine est un `erDiagram` Mermaid (faisant autorite, rendu
nativement) avec un rendu SVG portable dans `docs/merise/_diagrams/` ; voir la section 11 pour les
sources et la commande de regeneration.
---
## 4. Sub-domain: Catalogue
## 4. Sous-domaine : Catalogue
### 4.1 Mermaid entity-relationship diagram
### 4.1 Diagramme entite-relation Mermaid
```mermaid
erDiagram
@ -147,30 +147,30 @@ erDiagram
product ||--o{ menu_slot_option : "is_eligible_for"
```
### 4.2 Association cardinalities
### 4.2 Cardinalites des associations
| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification |
| # | Association | Cote A | Cardinalite A | Cote B | Cardinalite B | Justification |
|---|---|---|---|---|---|---|
| C1 | groups (product) | category | (0,N) | product | (1,1) | A category can exist with no products yet (created empty). A product must belong to exactly one category to appear on the kiosk. |
| C2 | groups (menu) | category | (0,N) | menu | (1,1) | Same rationale as C1 for menus. All 13 menus belong to the `menus` category. |
| C3 | anchors | menu | (1,1) | product | (0,N) | Each menu is built around exactly one fixed burger product (`burger_product_id`). A product may anchor 0 or more menus (a burger not used in a menu yet; or a popular burger anchoring several formats). |
| C4 | defines_slot | menu | (1,N) | menu_slot | (1,1) | A menu must define at least one slot (drink, side, sauce) to have customisable composition. A slot belongs to exactly one menu. |
| C5 | lists | menu_slot | (1,N) | menu_slot_option | (1,1) | A slot must list at least one eligible product (otherwise the customer cannot fill it). Each option row belongs to exactly one slot. |
| C6 | is_eligible_for | product | (0,N) | menu_slot_option | (1,1) | A product may be eligible for any number of slots across all menus, or none if it is only sold a la carte. Each option row references exactly one product. |
| C1 | groups (product) | category | (0,N) | product | (1,1) | Une categorie peut exister sans aucun produit pour l'instant (creee vide). Un produit doit appartenir a exactement une categorie pour apparaitre sur la borne. |
| C2 | groups (menu) | category | (0,N) | menu | (1,1) | Meme raisonnement que C1 pour les menus. Les 13 menus appartiennent a la categorie `menus`. |
| C3 | anchors | menu | (1,1) | product | (0,N) | Chaque menu est construit autour d'exactement un produit burger fixe (`burger_product_id`). Un produit peut ancrer 0 ou plusieurs menus (un burger pas encore utilise dans un menu ; ou un burger populaire ancrant plusieurs formats). |
| 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. |
| 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. |
### 4.3 Notes on the Catalogue sub-domain
### 4.3 Notes sur le sous-domaine Catalogue
**`menu_slot` vs category filter**: the explicit eligibility list `menu_slot_option(menu_slot_id, product_id)` was chosen over a category-based filter (`menu_slot.category_id`). Rationale: a product added to the `drinks` category should not automatically appear in every drink slot of every menu. The explicit list avoids accidental eligibility when the catalogue grows (see dictionary note 11).
**`menu_slot` vs filtre par categorie** : la liste d'eligibilite explicite `menu_slot_option(menu_slot_id, product_id)` a ete choisie plutot qu'un filtre base sur la categorie (`menu_slot.category_id`). Raisonnement : un produit ajoute a la categorie `drinks` ne devrait pas apparaitre automatiquement dans chaque slot boisson de chaque menu. La liste explicite evite une eligibilite accidentelle quand le catalogue s'agrandit (voir note 11 du dictionnaire).
**`menu.burger_product_id` as anchor**: the menu references a specific burger product, not a generic slot. This allows the ingredient configurator (sub-domain Ingredients & Stock) to resolve which ingredients are modifiable for a menu line, via `menu -> burger_product_id -> product_ingredient`.
**`menu.burger_product_id` comme ancre** : le menu reference un produit burger specifique, pas un slot generique. Cela permet au configurateur d'ingredients (sous-domaine Ingredients & Stock) de resoudre quels ingredients sont modifiables pour une ligne de menu, via `menu -> burger_product_id -> product_ingredient`.
**Normal / Maxi format**: two prices (`price_normal_cents`, `price_maxi_cents`) on `menu`; format recorded at `order_item.format`. No individual slot-level price differential is stored (see dictionary note 7).
**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).
---
## 5. Sub-domain: Ingredients & Stock
## 5. Sous-domaine : Ingredients & Stock
### 5.1 Mermaid entity-relationship diagram
### 5.1 Diagramme entite-relation Mermaid
```mermaid
erDiagram
@ -236,35 +236,35 @@ erDiagram
user |o--o{ stock_movement : "logs"
```
### 5.2 Association cardinalities
### 5.2 Cardinalites des associations
| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification |
| # | Association | Cote A | Cardinalite A | Cote B | Cardinalite B | Justification |
|---|---|---|---|---|---|---|
| I1 | is_composed_of | product | (0,N) | product_ingredient | (1,1) | A product may have no ingredients entered in the system yet (catalogue row exists before recipe is entered). A recipe row belongs to exactly one product. |
| I2 | appears_in | ingredient | (1,N) | product_ingredient | (1,1) | An ingredient in active use appears in at least one product recipe. Each recipe row references exactly one ingredient. Newly created ingredients with no recipe row yet are modelled as (0,N) from a pure structural standpoint; the business rule of (1,N) applies to ingredients in production use. |
| I3 | contains (allergens) | ingredient | (0,N) | ingredient_allergen | (1,1) | An ingredient may contain no regulated allergens (e.g., pure salt). Each allergen-link row belongs to one ingredient. |
| I4 | is_present_in | allergen | (0,N) | ingredient_allergen | (1,1) | An allergen may initially have no linked ingredients (seed: allergen catalogue is complete before recipe data is entered). Each link row references one allergen. |
| I5 | decrements | ingredient | (0,N) | stock_movement | (1,1) | All movements affect exactly one ingredient. An ingredient may have no stock movement rows yet if it was recently created and no orders have been placed. Each movement row references exactly one ingredient. |
| I6 | triggers | customer_order | (0,1) | stock_movement | (0,N) | A `sale` or `cancellation` movement references the originating order. A `restock` or `inventory_correction` has no order (NULL). A given order triggers movements across all its ingredients; an order still `pending_payment` has triggered no movement yet. |
| I7 | logs | user | (0,1) | stock_movement | (0,N) | Automated sale decrements have no user (NULL). Manual restocks and corrections are attributed to a user. A user may log any number of movements. |
| I1 | is_composed_of | product | (0,N) | product_ingredient | (1,1) | Un produit peut n'avoir aucun ingredient encore saisi dans le systeme (la ligne de catalogue existe avant que la recette ne soit saisie). Une ligne de recette appartient a exactement un produit. |
| I2 | appears_in | ingredient | (1,N) | product_ingredient | (1,1) | Un ingredient en usage actif apparait dans au moins une recette de produit. Chaque ligne de recette reference exactement un ingredient. Les ingredients nouvellement crees sans ligne de recette sont modelises en (0,N) d'un point de vue purement structurel ; la regle metier de (1,N) s'applique aux ingredients en usage de production. |
| I3 | contains (allergens) | ingredient | (0,N) | ingredient_allergen | (1,1) | Un ingredient peut ne contenir aucun allergene reglemente (par exemple, du sel pur). Chaque ligne de lien d'allergene appartient a un ingredient. |
| I4 | is_present_in | allergen | (0,N) | ingredient_allergen | (1,1) | Un allergene peut initialement n'avoir aucun ingredient lie (seed : le catalogue d'allergenes est complet avant que les donnees de recette ne soient saisies). Chaque ligne de lien reference un allergene. |
| I5 | decrements | ingredient | (0,N) | stock_movement | (1,1) | Tous les mouvements affectent exactement un ingredient. Un ingredient peut n'avoir encore aucune ligne de mouvement de stock s'il a ete cree recemment et qu'aucune commande n'a ete passee. Chaque ligne de mouvement reference exactement un ingredient. |
| I6 | triggers | customer_order | (0,1) | stock_movement | (0,N) | Un mouvement `sale` ou `cancellation` reference la commande d'origine. Un `restock` ou `inventory_correction` n'a pas de commande (NULL). Une commande donnee declenche des mouvements sur tous ses ingredients ; une commande encore `pending_payment` n'a declenche aucun mouvement. |
| I7 | logs | user | (0,1) | stock_movement | (0,N) | Les decrements de vente automatises n'ont pas d'utilisateur (NULL). Les reapprovisionnements et corrections manuels sont attribues a un utilisateur. Un utilisateur peut journaliser un nombre quelconque de mouvements. |
### 5.3 Notes on the Ingredients & Stock sub-domain
### 5.3 Notes sur le sous-domaine Ingredients & Stock
**`product_ingredient` as an associative entity**: the N-N association between `product` and `ingredient` carries five attributes (`quantity_normal`, `quantity_maxi`, `is_removable`, `is_addable`, `extra_price_cents`). It becomes a join table in the MLD with composite PK `(product_id, ingredient_id)`.
**`product_ingredient` comme entite associative** : l'association N-N entre `product` et `ingredient` porte cinq attributs (`quantity_normal`, `quantity_maxi`, `is_removable`, `is_addable`, `extra_price_cents`). Elle devient une table de jointure dans le MLD avec une PK composite `(product_id, ingredient_id)`.
**`ingredient_allergen` as a pure join table**: no own attributes. The allergen set for a product is computed at query time by joining `product_ingredient -> ingredient_allergen -> allergen`; no manual per-product entry is needed.
**`ingredient_allergen` comme table de jointure pure** : aucun attribut propre. L'ensemble des allergenes d'un produit est calcule au moment de la requete en joignant `product_ingredient -> ingredient_allergen -> allergen` ; aucune saisie manuelle par produit n'est necessaire.
**`stock_movement` immutability**: this table is append-only. No UPDATE or DELETE is permitted at application layer. Corrections are new rows with `movement_type = 'inventory_correction'` and a signed `delta`.
**Immuabilite de `stock_movement`** : cette table est append-only. Aucun UPDATE ni DELETE n'est autorise au niveau applicatif. Les corrections sont de nouvelles lignes avec `movement_type = 'inventory_correction'` et un `delta` signe.
**Percentage-based stock model**: stock health is anchored on a per-ingredient `stock_capacity` (the 100% reference, `CHECK > 0`). `stock_quantity` is signed and may go negative when sales outrun counted stock; the system does not block an order on a low stock read. `stock_pct = ROUND(stock_quantity / stock_capacity * 100)` is computed, not stored. Two percentage thresholds drive a three-band behaviour: `low_stock_pct` (warning band, default 10%) and `critical_stock_pct` (auto-out-of-stock floor, default 5%), with the table-level invariant `critical_stock_pct < low_stock_pct`. Above the low band is normal; between critical and low the product stays orderable and a manager alert is raised (the manager either pulls the product via `product.is_available = 0` or restocks to clear the alert); at or below the critical band the product auto-goes out-of-stock (computed, see below).
**Modele de stock base sur les pourcentages** : la sante du stock est ancree sur une `stock_capacity` par ingredient (la reference 100%, `CHECK > 0`). `stock_quantity` est signe et peut devenir negatif quand les ventes depassent le stock compte ; le systeme ne bloque pas une commande sur une lecture de stock bas. `stock_pct = ROUND(stock_quantity / stock_capacity * 100)` est calcule, pas stocke. Deux seuils en pourcentage pilotent un comportement a trois bandes : `low_stock_pct` (bande d'alerte, defaut 10%) et `critical_stock_pct` (plancher de mise en rupture automatique, defaut 5%), avec l'invariant au niveau de la table `critical_stock_pct < low_stock_pct`. Au-dessus de la bande d'alerte, c'est normal ; entre critique et bas, le produit reste commandable et une alerte manager est levee (le manager soit retire le produit via `product.is_available = 0`, soit reapprovisionne pour lever l'alerte) ; au niveau ou en dessous de la bande critique, le produit passe automatiquement en rupture (calcule, voir ci-dessous).
**Computed product availability (rule RG-T21, see `mlt.md`)**: effective orderability is derived, not stored. A product is orderable when `product.is_available = 1` AND each non-removable (`is_removable = 0`) ingredient in its `product_ingredient` has `stock_quantity > stock_capacity * critical_stock_pct / 100`. A required ingredient reaching the critical band makes the product auto-out-of-stock with no write and no cascade; a manual pull (`product.is_available = 0`) is a hard override; restock above the critical band makes the product orderable again on its own. A removable/optional ingredient at the critical band does not block the product (only its add-on becomes unavailable). The dashboard distinguishes a manual pull from a stock-driven OOS.
**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.
---
## 6. Sub-domain: Order
## 6. Sous-domaine : Order
### 6.1 Mermaid entity-relationship diagram
### 6.1 Diagramme entite-relation Mermaid
```mermaid
erDiagram
@ -336,55 +336,55 @@ erDiagram
ingredient ||--o{ order_item_modifier : "modified_by"
```
### 6.2 Association cardinalities
### 6.2 Cardinalites des associations
| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification |
| # | Association | Cote A | Cardinalite A | Cote B | Cardinalite B | Justification |
|---|---|---|---|---|---|---|
| O1 | contains | customer_order | (1,N) | order_item | (1,1) | An order without at least one line has no business meaning. A line belongs to exactly one order. ON DELETE CASCADE: if the order is purged, its lines go with it. |
| O2 | references_product | order_item | (0,1) | product | (0,N) | When `item_type = 'product'`, `product_id` is non-null (1 product referenced). When `item_type = 'menu'`, `product_id` is NULL (0). A product may appear in any number of order lines across history. |
| O3 | references_menu | order_item | (0,1) | menu | (0,N) | Symmetric to O2 for the menu discriminator branch. Exactly one of O2/O3 is active per line (CHECK constraint in MLD). |
| O4 | fills_slot | order_item | (0,N) | order_item_selection | (1,1) | A `menu`-type order line has one selection per slot (typically 2-3). A `product`-type line has no selections (0). Each selection row belongs to exactly one order line. |
| O5 | slot_filled_by | menu_slot | (0,N) | order_item_selection | (1,1) | A slot definition may have been chosen many times across historical orders (0,N). Each selection row references exactly one slot. ON DELETE RESTRICT: preserves historical records if the slot definition is later changed. |
| O6 | chosen_for_slot | product | (0,N) | order_item_selection | (1,1) | A product may have been selected for many slot choices across history. Each selection references one product. |
| O7 | modifies_ingredient | order_item | (0,N) | order_item_modifier | (1,1) | An order line may have any number of ingredient modifications (remove onion, add cheese). Each modifier row belongs to one order line. |
| O8 | modified_by | ingredient | (0,N) | order_item_modifier | (1,1) | An ingredient may have been modified in many order lines across history. Each modifier references one ingredient. |
| O1 | contains | customer_order | (1,N) | order_item | (1,1) | Une commande sans au moins une ligne n'a aucun sens metier. Une ligne appartient a exactement une commande. ON DELETE CASCADE : si la commande est purgee, ses lignes partent avec elle. |
| O2 | references_product | order_item | (0,1) | product | (0,N) | Quand `item_type = 'product'`, `product_id` est non nul (1 produit reference). Quand `item_type = 'menu'`, `product_id` est NULL (0). Un produit peut apparaitre dans un nombre quelconque de lignes de commande a travers l'historique. |
| O3 | references_menu | order_item | (0,1) | menu | (0,N) | Symetrique a O2 pour la branche du discriminateur menu. Exactement un de O2/O3 est actif par ligne (contrainte CHECK dans le MLD). |
| O4 | fills_slot | order_item | (0,N) | order_item_selection | (1,1) | Une ligne de commande de type `menu` a une selection par slot (typiquement 2-3). Une ligne de type `product` n'a aucune selection (0). Chaque ligne de selection appartient a exactement une ligne de commande. |
| O5 | slot_filled_by | menu_slot | (0,N) | order_item_selection | (1,1) | Une definition de slot peut avoir ete choisie de nombreuses fois a travers les commandes historiques (0,N). Chaque ligne de selection reference exactement un slot. ON DELETE RESTRICT : preserve les enregistrements historiques si la definition de slot est modifiee ulterieurement. |
| O6 | chosen_for_slot | product | (0,N) | order_item_selection | (1,1) | Un produit peut avoir ete selectionne pour de nombreux choix de slot a travers l'historique. Chaque selection reference un produit. |
| O7 | modifies_ingredient | order_item | (0,N) | order_item_modifier | (1,1) | Une ligne de commande peut avoir un nombre quelconque de modifications d'ingredients (retirer l'oignon, ajouter du fromage). Chaque ligne de modificateur appartient a une ligne de commande. |
| O8 | modified_by | ingredient | (0,N) | order_item_modifier | (1,1) | Un ingredient peut avoir ete modifie dans de nombreuses lignes de commande a travers l'historique. Chaque modificateur reference un ingredient. |
### 6.3 Notes on the Order sub-domain
### 6.3 Notes sur le sous-domaine Order
**Polymorphism on `order_item`**: each line references either a `product` or a `menu` (not both, not neither). The discriminator `item_type` ENUM drives which FK is populated. The mutual exclusivity is enforced by a CHECK constraint in the MLD. This pattern (2 nullable FKs + discriminator + CHECK) is a standard relational approach to single-table inheritance without a separate table per type.
**Polymorphisme sur `order_item`** : chaque ligne reference soit un `product`, soit un `menu` (ni les deux, ni aucun). Le discriminateur `item_type` ENUM pilote quelle FK est renseignee. L'exclusivite mutuelle est imposee par une contrainte CHECK dans le MLD. Ce pattern (2 FK nullables + discriminateur + CHECK) est une approche relationnelle standard de l'heritage en table unique sans table separee par type.
**`order_item_selection` (menu slot choices)**: captures which product the customer chose for each slot of a menu line. One row per slot filled. Used for KPI analysis (most popular drink/side combinations). The `label_snapshot` preserves the product name at transaction time.
**`order_item_selection` (choix de slot de menu)** : capture quel produit le client a choisi pour chaque slot d'une ligne de menu. Une ligne par slot rempli. Utilise pour l'analyse de KPI (combinaisons boisson/accompagnement les plus populaires). Le `label_snapshot` preserve le nom du produit au moment de la transaction.
**`order_item_modifier` (ingredient modifications)**: attaches to an `order_item` regardless of whether the line is a standalone product or a menu. For a menu line, the modifiable product is the fixed burger, resolved via `order_item.menu_id -> menu.burger_product_id` (see dictionary note 10). No additional FK column is needed on `order_item_modifier`.
**`order_item_modifier` (modifications d'ingredients)** : se rattache a un `order_item` que la ligne soit un produit autonome ou un menu. Pour une ligne de menu, le produit modifiable est le burger fixe, resolu via `order_item.menu_id -> menu.burger_product_id` (voir note 10 du dictionnaire). Aucune colonne FK supplementaire n'est necessaire sur `order_item_modifier`.
**Price snapshots**: `label_snapshot`, `unit_price_cents_snapshot`, and `vat_rate_snapshot` on `order_item` preserve the state at transaction time. If a product is later renamed or repriced, historical order data remains consistent. ON DELETE RESTRICT on `product_id` and `menu_id` is a secondary safeguard.
**Snapshots de prix** : `label_snapshot`, `unit_price_cents_snapshot`, et `vat_rate_snapshot` sur `order_item` preservent l'etat au moment de la transaction. Si un produit est ulterieurement renomme ou retarife, les donnees de commande historiques restent coherentes. ON DELETE RESTRICT sur `product_id` et `menu_id` est une protection secondaire.
**`service_day` computation** (KPI grouping): not stored as a column. Computed at query time:
**Calcul de `service_day`** (regroupement KPI) : non stocke comme colonne. Calcule au moment de la requete :
```sql
CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END
```
Cutoff: 10:00. The generated-column formula with `INTERVAL 4 HOUR 30 MINUTE` from the v0.1 MLD
was incorrect and is dropped (decision D6, `revue-alignement-p1.md` §7).
Seuil : 10:00. La formule de colonne generee avec `INTERVAL 4 HOUR 30 MINUTE` du MLD v0.1
etait incorrecte et est abandonnee (decision D6, `revue-alignement-p1.md` §7).
**`source = 'drive' => service_mode = 'drive'`**: cross-constraint. A drive-channel order can
only have `service_mode = 'drive'`. Enforced at application layer (and optionally as a CHECK in
the MLD).
**`source = 'drive' => service_mode = 'drive'`** : contrainte croisee. Une commande du canal drive ne peut
avoir que `service_mode = 'drive'`. Imposee au niveau applicatif (et optionnellement comme CHECK dans
le MLD).
**4-state machine** (`pending_payment -> paid -> delivered` + `cancelled`):
`preparing` and `ready` are dropped (decision D4, `revue-alignement-p1.md` §7). KPI timing is
`delivered_at - paid_at`; KDS colour coding is computed from `NOW() - paid_at`.
**Machine a 4 etats** (`pending_payment -> paid -> delivered` + `cancelled`) :
`preparing` et `ready` sont abandonnes (decision D4, `revue-alignement-p1.md` §7). Le timing KPI est
`delivered_at - paid_at` ; le codage couleur KDS est calcule a partir de `NOW() - paid_at`.
**Security-by-design columns (2026-06-11)**: `idempotency_key` (client UUID, UNIQUE)
deduplicates a retried `POST /api/orders`. `acting_user_id` (FK -> `user`, ON DELETE SET NULL)
records the counter/drive staff who took the order under PIN; NULL for anonymous kiosk orders.
This adds a `customer_order |o--o| user : "taken_by"` association (cardinality: an order is
taken by (0,1) user; a user takes (0,N) orders). See dictionary note 13.
**Colonnes security-by-design (2026-06-11)** : `idempotency_key` (UUID client, UNIQUE)
deduplique un `POST /api/orders` rejoue. `acting_user_id` (FK -> `user`, ON DELETE SET NULL)
enregistre l'employe de comptoir/drive qui a pris la commande sous PIN ; NULL pour les commandes anonymes de la borne.
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.
---
## 7. Sub-domain: RBAC
## 7. Sous-domaine : RBAC
### 7.1 Mermaid entity-relationship diagram
### 7.1 Diagramme entite-relation Mermaid
```mermaid
erDiagram
@ -453,175 +453,175 @@ erDiagram
role |o--o{ audit_log : "context_of"
```
> `login_throttle` is a standalone entity with no association: it is keyed by source IP
> (`ip_address UNIQUE`), not by a modelled actor, so it carries no FK and connects to no
> other entity in the diagram.
> `login_throttle` est une entite autonome sans association : elle est indexee par IP source
> (`ip_address UNIQUE`), pas par un acteur modelise, donc elle ne porte aucune FK et ne se connecte a aucune
> autre entite du diagramme.
### 7.2 Association cardinalities
### 7.2 Cardinalites des associations
| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification |
| # | Association | Cote A | Cardinalite A | Cote B | Cardinalite B | Justification |
|---|---|---|---|---|---|---|
| R1 | holds | user | (1,1) | role | (0,N) | A user must have exactly one role to access the back-office. A role may have no current users (created but not yet assigned). ON DELETE RESTRICT on `role_id`: a role cannot be deleted while users hold it. |
| R2 | sees_source | role | (0,N) | role_visible_source | (1,1) | A role may see 0 or more order sources on the preparation dashboard (admin/manager use a global view with no source filter). Each visibility row belongs to exactly one role. |
| R3 | grants | role | (0,N) | role_permission | (1,1) | A role may have no permissions (a newly created role before assignment) or many. Each mapping row belongs to one role. |
| R4 | granted_to | permission | (0,N) | role_permission | (1,1) | A permission may be granted to no roles yet (declared at seed, not yet distributed) or to several. Each mapping row references one permission. |
| R5 | performs | user | (0,1) | audit_log | (0,N) | A sensitive action captured under PIN records its acting user; automated/non-attributable entries carry NULL. A user may have logged any number of actions. ON DELETE SET NULL preserves the trail on user anonymisation/removal. |
| R6 | context_of | role | (0,1) | audit_log | (0,N) | Each audit row may denormalise the actor's role at action time (NULL allowed). A role may be the context of many audit rows. ON DELETE SET NULL preserves the trail. |
| R1 | holds | user | (1,1) | role | (0,N) | Un utilisateur doit avoir exactement un role pour acceder au back-office. Un role peut n'avoir aucun utilisateur actuel (cree mais pas encore assigne). ON DELETE RESTRICT sur `role_id` : un role ne peut etre supprime tant que des utilisateurs le detiennent. |
| R2 | sees_source | role | (0,N) | role_visible_source | (1,1) | Un role peut voir 0 ou plusieurs sources de commande sur le tableau de bord de preparation (admin/manager utilisent une vue globale sans filtre de source). Chaque ligne de visibilite appartient a exactement un role. |
| R3 | grants | role | (0,N) | role_permission | (1,1) | Un role peut n'avoir aucune permission (un role nouvellement cree avant assignation) ou plusieurs. Chaque ligne de mapping appartient a un role. |
| R4 | granted_to | permission | (0,N) | role_permission | (1,1) | Une permission peut n'etre encore accordee a aucun role (declaree au seed, pas encore distribuee) ou a plusieurs. Chaque ligne de mapping reference une permission. |
| R5 | performs | user | (0,1) | audit_log | (0,N) | Une action sensible capturee sous PIN enregistre son utilisateur agissant ; les entrees automatisees/non attribuables portent NULL. Un utilisateur peut avoir journalise un nombre quelconque d'actions. ON DELETE SET NULL preserve la trace lors de l'anonymisation/suppression de l'utilisateur. |
| R6 | context_of | role | (0,1) | audit_log | (0,N) | Chaque ligne d'audit peut denormaliser le role de l'acteur au moment de l'action (NULL autorise). Un role peut etre le contexte de nombreuses lignes d'audit. ON DELETE SET NULL preserve la trace. |
### 7.3 Notes on the RBAC sub-domain
### 7.3 Notes sur le sous-domaine RBAC
**RBAC architecture**: roles are dynamic (creatable and modifiable via admin UI). Permissions are static (declared in migration, tied to application code). Application code tests permissions, not role names: adding a new role with the right permissions requires no code change (permission-driven, per Sandhu/NIST RBAC model — decision D4, `revue-alignement-p1.md` §7).
**Architecture RBAC** : les roles sont dynamiques (creables et modifiables via l'UI admin). Les permissions sont statiques (declarees en migration, liees au code applicatif). Le code applicatif teste les permissions, pas les noms de role : ajouter un nouveau role avec les bonnes permissions ne necessite aucun changement de code (permission-driven, selon le modele RBAC Sandhu/NIST — decision D4, `revue-alignement-p1.md` §7).
**`role.order_source`**: when a counter or drive staff member creates an order, the `source` column on `customer_order` is automatically populated from their role's `order_source`. NULL for admin and manager (they can create on behalf of any channel).
**`role.order_source`** : quand un employe de comptoir ou de drive cree une commande, la colonne `source` sur `customer_order` est automatiquement renseignee a partir de l'`order_source` de son role. NULL pour admin et manager (ils peuvent creer pour le compte de n'importe quel canal).
**`role.default_route`**: the landing screen for each role, stored in the database. Front-end routing reads this value at login; no role name is hardcoded in routing logic.
**`role.default_route`** : l'ecran d'arrivee pour chaque role, stocke en base de donnees. Le routage front-end lit cette valeur au login ; aucun nom de role n'est code en dur dans la logique de routage.
**`role_visible_source`**: a pure join table linking a role to the set of order sources visible on the preparation dashboard. A `kitchen` role sees all three sources; a `counter` role sees `kiosk` and `counter`; a `drive` role sees only `drive`.
**`role_visible_source`** : une table de jointure pure liant un role a l'ensemble des sources de commande visibles sur le tableau de bord de preparation. Un role `kitchen` voit les trois sources ; un role `counter` voit `kiosk` et `counter` ; un role `drive` ne voit que `drive`.
**`role_permission`** and **`role_visible_source`** both use composite PKs. ON DELETE CASCADE on both FKs of `role_permission` (deleting a role or a permission removes its mappings). ON DELETE CASCADE on `role_id` of `role_visible_source`.
**`role_permission`** et **`role_visible_source`** utilisent tous deux des PK composites. ON DELETE CASCADE sur les deux FK de `role_permission` (supprimer un role ou une permission retire ses mappings). ON DELETE CASCADE sur le `role_id` de `role_visible_source`.
**Seed roles** (5 roles, frozen at DDL; extendable without code change):
**Roles de seed** (5 roles, figes au DDL ; extensibles sans changement de code) :
`admin`, `manager`, `kitchen`, `counter`, `drive`.
**`audit_log` (security-by-design)**: append-only log of sensitive actions, immutable like
`stock_movement`. Both FKs (`actor_user_id`, `actor_role_id`) are nullable with ON DELETE
SET NULL, so the trail survives user anonymisation (RGPD) and role removal. The `actor_role_id`
is denormalised on purpose: even if the user is later anonymised, the role context of the
action is preserved. It carries no PII (the `details` JSON stores changed field names, not
values for user-targeted actions). See dictionary 3.20 and note 13.
**`audit_log` (security-by-design)** : journal append-only des actions sensibles, immuable comme
`stock_movement`. Les deux FK (`actor_user_id`, `actor_role_id`) sont nullables avec ON DELETE
SET NULL, de sorte que la trace survit a l'anonymisation de l'utilisateur (RGPD) et a la suppression de role. Le `actor_role_id`
est denormalise a dessein : meme si l'utilisateur est ulterieurement anonymise, le contexte de role de
l'action est preserve. Il ne porte aucune PII (le JSON `details` stocke les noms des champs modifies, pas les
valeurs pour les actions ciblant un utilisateur). Voir dictionnaire 3.20 et note 13.
**`login_throttle` (security-by-design)**: per-source-IP brute-force throttle, complementing
the per-account counter already on `user` (`failed_login_attempts` / `lockout_until`). One row
per IP (`ip_address VARCHAR(45) UNIQUE`, 45 chars to hold a full IPv6 literal), upserted on each
failed login: `failed_attempts` counts consecutive failures from this IP in the current window,
`window_started_at` marks the start of that window (which resets when expired), `lockout_until`
holds the end of the degressive backoff (NULL = not throttled), `last_attempt_at` the timestamp
of the last failed attempt. It has no FK (an IP is not a modelled entity) and no association. A
daily cron purges rows with no active lockout whose `last_attempt_at` is older than 24h. See
dictionary 3.21 and note 13.
**`login_throttle` (security-by-design)** : throttle anti-brute-force par IP source, complementant
le compteur par compte deja present sur `user` (`failed_login_attempts` / `lockout_until`). Une ligne
par IP (`ip_address VARCHAR(45) UNIQUE`, 45 caracteres pour contenir un litteral IPv6 complet), upsertee a chaque
echec de login : `failed_attempts` compte les echecs consecutifs depuis cette IP dans la fenetre courante,
`window_started_at` marque le debut de cette fenetre (qui se reinitialise a son expiration), `lockout_until`
contient la fin du backoff degressif (NULL = non throttle), `last_attempt_at` l'horodatage
de la derniere tentative echouee. Elle n'a aucune FK (une IP n'est pas une entite modelisee) et aucune association. Un
cron quotidien purge les lignes sans lockout actif dont le `last_attempt_at` est plus ancien que 24h. Voir
dictionnaire 3.21 et note 13.
---
## 8. Cross-validation MCD <-> dictionary
## 8. Validation croisee MCD <-> dictionnaire
Verification that all 21 dictionary entities appear in the MCD and vice versa.
Verification que les 21 entites du dictionnaire apparaissent dans le MCD et reciproquement.
| # | Dictionary entity (section 3) | Sub-domain in MCD | Present |
| # | Entite du dictionnaire (section 3) | Sous-domaine dans le MCD | Presente |
|---|---|---|---|
| 1 | `category` (3.1) | Catalogue | Yes |
| 2 | `product` (3.2) | Catalogue + Ingredients + Order | Yes |
| 3 | `menu` (3.3) | Catalogue + Order | Yes |
| 4 | `menu_slot` (3.4) | Catalogue + Order | Yes |
| 5 | `menu_slot_option` (3.5) | Catalogue | Yes |
| 6 | `ingredient` (3.6) | Ingredients + Order | Yes |
| 7 | `product_ingredient` (3.7) | Ingredients | Yes |
| 8 | `allergen` (3.8) | Ingredients | Yes |
| 9 | `ingredient_allergen` (3.9) | Ingredients | Yes |
| 10 | `customer_order` (3.10) | Order | Yes |
| 11 | `order_item` (3.11) | Order | Yes |
| 12 | `order_item_selection` (3.12) | Order | Yes |
| 13 | `order_item_modifier` (3.13) | Order | Yes |
| 14 | `user` (3.14) | RBAC | Yes |
| 15 | `role` (3.15) | RBAC | Yes |
| 16 | `role_visible_source` (3.16) | RBAC | Yes |
| 17 | `permission` (3.17) | RBAC | Yes |
| 18 | `role_permission` (3.18) | RBAC | Yes |
| 19 | `stock_movement` (3.19) | Ingredients & Stock | Yes |
| 20 | `audit_log` (3.20) | RBAC & Audit | Yes |
| 21 | `login_throttle` (3.21) | RBAC & Audit | Yes |
| 1 | `category` (3.1) | Catalogue | Oui |
| 2 | `product` (3.2) | Catalogue + Ingredients + Order | Oui |
| 3 | `menu` (3.3) | Catalogue + Order | Oui |
| 4 | `menu_slot` (3.4) | Catalogue + Order | Oui |
| 5 | `menu_slot_option` (3.5) | Catalogue | Oui |
| 6 | `ingredient` (3.6) | Ingredients + Order | Oui |
| 7 | `product_ingredient` (3.7) | Ingredients | Oui |
| 8 | `allergen` (3.8) | Ingredients | Oui |
| 9 | `ingredient_allergen` (3.9) | Ingredients | Oui |
| 10 | `customer_order` (3.10) | Order | Oui |
| 11 | `order_item` (3.11) | Order | Oui |
| 12 | `order_item_selection` (3.12) | Order | Oui |
| 13 | `order_item_modifier` (3.13) | Order | Oui |
| 14 | `user` (3.14) | RBAC | Oui |
| 15 | `role` (3.15) | RBAC | Oui |
| 16 | `role_visible_source` (3.16) | RBAC | Oui |
| 17 | `permission` (3.17) | RBAC | Oui |
| 18 | `role_permission` (3.18) | RBAC | Oui |
| 19 | `stock_movement` (3.19) | Ingredients & Stock | Oui |
| 20 | `audit_log` (3.20) | RBAC & Audit | Oui |
| 21 | `login_throttle` (3.21) | RBAC & Audit | Oui |
**Result**: 21/21 entities traced (19 prod-like + `audit_log` and `login_throttle`
security-by-design). No entity from the dictionary is absent from the MCD. No entity in the MCD
falls outside the dictionary.
**Resultat** : 21/21 entites tracees (19 prod-like + `audit_log` et `login_throttle`
security-by-design). Aucune entite du dictionnaire n'est absente du MCD. Aucune entite du MCD
ne tombe en dehors du dictionnaire.
**Entities appearing in multiple sub-domains** (cross-domain shared entities):
- `product`: Catalogue (sold item, slot eligibility) + Ingredients (recipe) + Order (line reference, slot choice)
- `menu`: Catalogue (definition, slots) + Order (line reference)
- `menu_slot`: Catalogue (slot definition) + Order (slot choices via `order_item_selection`)
- `ingredient`: Ingredients (recipe, stock) + Order (modifiers)
- `customer_order`: Order (order lifecycle) + Ingredients (stock movement trigger) + RBAC & Audit (taken_by staff via `acting_user_id`)
- `user`: RBAC (authentication) + Ingredients (stock movement author) + Order (`acting_user_id` on counter/drive orders) + Audit (actor of `audit_log`)
- `role`: RBAC (permissions, visible sources) + Audit (denormalised `actor_role_id` context on `audit_log`)
**Entites apparaissant dans plusieurs sous-domaines** (entites partagees inter-domaines) :
- `product` : Catalogue (article vendu, eligibilite de slot) + Ingredients (recette) + Order (reference de ligne, choix de slot)
- `menu` : Catalogue (definition, slots) + Order (reference de ligne)
- `menu_slot` : Catalogue (definition de slot) + Order (choix de slot via `order_item_selection`)
- `ingredient` : Ingredients (recette, stock) + Order (modificateurs)
- `customer_order` : Order (cycle de vie de la commande) + Ingredients (declencheur de mouvement de stock) + RBAC & Audit (employe taken_by via `acting_user_id`)
- `user` : RBAC (authentification) + Ingredients (auteur de mouvement de stock) + Order (`acting_user_id` sur les commandes comptoir/drive) + Audit (acteur de `audit_log`)
- `role` : RBAC (permissions, sources visibles) + Audit (contexte `actor_role_id` denormalise sur `audit_log`)
This is expected in a normalised model. The sub-domain split is for readability; the actual
relational schema is a unified graph.
C'est attendu dans un modele normalise. La division par sous-domaine est pour la lisibilite ; le schema
relationnel reel est un graphe unifie.
---
## 9. Decisions deferred to the MLD
## 9. Decisions reportees au MLD
The MCD remains at the conceptual level. The following decisions are deferred to the MLD:
Le MCD reste au niveau conceptuel. Les decisions suivantes sont reportees au MLD :
1. **Resolution of associative entities into tables**: `product_ingredient`, `menu_slot_option`,
`ingredient_allergen`, `role_visible_source`, `role_permission` become join tables with
composite PKs.
2. **Technical PK vs business identifier**: `id INT UNSIGNED AUTO_INCREMENT` on all main entities.
`customer_order` additionally carries `order_number VARCHAR(20) UNIQUE` (human-readable,
format `K/C/D-YYYY-MM-DD-NNN` per channel).
3. **ON DELETE rules**: CASCADE vs RESTRICT vs SET NULL. Detailed in the MLD.
4. **CHECK constraints**: polymorphism exclusivity on `order_item`, cross-constraint
`source/service_mode` on `customer_order`, arithmetic invariant on totals.
5. **Indexes**: not discussed at MCD level. Defined in the MLD for frequent query patterns.
6. **`service_day` formula**: applicative CASE expression, not a stored generated column.
Documented in the MLD.
1. **Resolution des entites associatives en tables** : `product_ingredient`, `menu_slot_option`,
`ingredient_allergen`, `role_visible_source`, `role_permission` deviennent des tables de jointure avec
des PK composites.
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,
format `K/C/D-YYYY-MM-DD-NNN` par canal).
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
`source/service_mode` sur `customer_order`, invariant arithmetique sur les totaux.
5. **Index** : non discutes au niveau MCD. Definis dans le MLD pour les patterns de requete frequents.
6. **Formule `service_day`** : expression applicative CASE, pas une colonne generee stockee.
Documentee dans le MLD.
---
## 10. MCD <-> MCT coherence (mantra #34)
## 10. Coherence MCD <-> MCT (mantra #34)
Pre-validation: each entity participates in at least one treatment.
Pre-validation : chaque entite participe a au moins un traitement.
| Entity | Expected treatment(s) |
| Entite | Traitement(s) attendu(s) |
|---|---|
| `category` | Admin CRUD |
| `product` | Admin CRUD + kiosk cart add |
| `menu` | Admin CRUD + kiosk cart add |
| `menu_slot` | Admin CRUD (menu composition) |
| `menu_slot_option` | Admin CRUD (slot eligibility management) |
| `ingredient` | Admin CRUD + stock movements |
| `product_ingredient` | Admin recipe management |
| `allergen` | Admin CRUD (seed: read-only catalogue) |
| `ingredient_allergen` | Admin allergen mapping |
| `customer_order` | Full order lifecycle (create -> pay -> deliver / cancel) |
| `order_item` | Cart building, line creation at validation |
| `order_item_selection` | Menu slot selection during cart building |
| `order_item_modifier` | Ingredient modification during cart building |
| `user` | Admin CRUD + login |
| `role` | Admin CRUD + user assignment |
| `role_visible_source` | Admin role configuration |
| `permission` | Admin permission matrix management |
| `role_permission` | Admin permission matrix management |
| `stock_movement` | Automatic at `paid` transition; manual restock and inventory correction |
| `audit_log` | Written by sensitive operations: UPDATE/DELETE product/menu (8.2/8.3/8.6), CANCEL_ORDER (7.1), RESTOCK/INVENTORY_COUNT (9.1/9.2), user ops (10.1-10.3), MANAGE_RBAC (10.4), and failed/successful logins (12.1) |
| `login_throttle` | Read and written by AUTHENTICATE_USER (12.1): per-source-IP throttle upserted on each failed login, read to enforce the backoff window, purged by a daily cron |
| `category` | CRUD admin |
| `product` | CRUD admin + ajout au panier borne |
| `menu` | CRUD admin + ajout au panier borne |
| `menu_slot` | CRUD admin (composition de menu) |
| `menu_slot_option` | CRUD admin (gestion de l'eligibilite des slots) |
| `ingredient` | CRUD admin + mouvements de stock |
| `product_ingredient` | Gestion des recettes admin |
| `allergen` | CRUD admin (seed : catalogue en lecture seule) |
| `ingredient_allergen` | Mapping des allergenes admin |
| `customer_order` | Cycle de vie complet de la commande (create -> pay -> deliver / cancel) |
| `order_item` | Construction du panier, creation de ligne a la validation |
| `order_item_selection` | Selection de slot de menu pendant la construction du panier |
| `order_item_modifier` | Modification d'ingredient pendant la construction du panier |
| `user` | CRUD admin + login |
| `role` | CRUD admin + assignation d'utilisateur |
| `role_visible_source` | Configuration de role admin |
| `permission` | Gestion de la matrice de permissions admin |
| `role_permission` | Gestion de la matrice de permissions admin |
| `stock_movement` | Automatique a la transition `paid` ; reapprovisionnement manuel et correction d'inventaire |
| `audit_log` | Ecrit par les operations sensibles : UPDATE/DELETE product/menu (8.2/8.3/8.6), CANCEL_ORDER (7.1), RESTOCK/INVENTORY_COUNT (9.1/9.2), operations utilisateur (10.1-10.3), MANAGE_RBAC (10.4), et logins echoues/reussis (12.1) |
| `login_throttle` | Lu et ecrit par AUTHENTICATE_USER (12.1) : throttle par IP source upserte a chaque echec de login, lu pour imposer la fenetre de backoff, purge par un cron quotidien |
Cross-validation MCD <-> MCT (mantra #34) to be completed exhaustively in `mct.md`
once the MCT incorporates the security-by-design operations (PIN-gated sensitive actions,
audit writes, reset/lockout, anonymisation). The treatment-layer additions are tracked there.
La validation croisee MCD <-> MCT (mantra #34) sera completee de maniere exhaustive dans `mct.md`
une fois que le MCT integrera les operations security-by-design (actions sensibles protegees par PIN,
ecritures d'audit, reset/lockout, anonymisation). Les ajouts de la couche traitements y sont suivis.
---
## 11. Diagram sources and regeneration
## 11. Sources des diagrammes et regeneration
The authoritative graphical model is the set of Mermaid `erDiagram` blocks in sections 4-7,
one per sub-domain. They render natively on Forgejo and GitHub. The MCD is decomposed by
sub-domain on purpose: a single 21-entity diagram cannot be laid out without crossing
relationship lines (intrinsic planarity limit, and `erDiagram` offers no manual layout
control). Each sub-domain stays at 5-8 entities, which auto-layout handles cleanly. The
integrated view across sub-domains is the cross-validation table in section 8.
Le modele graphique faisant autorite est l'ensemble des blocs `erDiagram` Mermaid des sections 4-7,
un par sous-domaine. Ils s'affichent nativement sur Forgejo et GitHub. Le MCD est decompose par
sous-domaine a dessein : un unique diagramme de 21 entites ne peut etre dispose sans croisement de
lignes de relation (limite de planarite intrinseque, et `erDiagram` n'offre aucun controle de mise en page
manuel). Chaque sous-domaine reste a 5-8 entites, ce que la mise en page automatique gere proprement. La
vue integree a travers les sous-domaines est la table de validation croisee de la section 8.
Portable SVG renders live in `docs/merise/_diagrams/` (for PDF export / offline viewing):
Des rendus SVG portables se trouvent dans `docs/merise/_diagrams/` (pour l'export PDF / consultation hors ligne) :
| Sub-domain | Source | Render |
| Sous-domaine | Source | Rendu |
|---|---|---|
| Catalogue | `mcd-catalogue.mmd` | `mcd-catalogue.svg` |
| Ingredients & Stock | `mcd-ingredients-stock.mmd` | `mcd-ingredients-stock.svg` |
| Order | `mcd-order.mmd` | `mcd-order.svg` |
| RBAC | `mcd-rbac.mmd` | `mcd-rbac.svg` |
The `.mmd` files are extracted from the `erDiagram` blocks above; the `.svg` are produced by
`make docs-render` (mmdc). If a block here changes, re-extract the matching `.mmd` and re-run
`make docs-render`. The legacy v0.1 `.drawio` sources have been removed: drawio gave manual
layout control but required hand-editing and did not render in the Markdown previews, whereas
the decomposed Mermaid blocks are version-controlled, render everywhere, and stay in sync with
this document.
Les fichiers `.mmd` sont extraits des blocs `erDiagram` ci-dessus ; les `.svg` sont produits par
`make docs-render` (mmdc). Si un bloc ici change, re-extraire le `.mmd` correspondant et relancer
`make docs-render`. Les anciennes sources `.drawio` v0.1 ont ete supprimees : drawio offrait un controle de mise en page
manuel mais necessitait une edition a la main et ne s'affichait pas dans les apercus Markdown, alors que
les blocs Mermaid decomposes sont versionnes, s'affichent partout, et restent synchronises avec
ce document.

View file

@ -1,44 +1,44 @@
# Model of Conceptual Treatments (MCT) — Wakdo
# Modele Conceptuel des Traitements (MCT) — Wakdo
**Merise phase** : P1 - Conception, step 3 (after MCD)
**Version** : v0.2 — prod-like, 4-state machine (+ security-by-design layer 2026-06-11)
**Date** : 2026-06-04 (security-by-design additions 2026-06-11)
**Branch** : `feat/p1-conception`
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7); security-by-design ops added (ERASE_USER_PII, RESET_PASSWORD, PIN-gated sensitive set, audit_log writes, auth throttling) — 28 operations
**Author** : BYAN (methodology layer)
**Phase Merise** : P1 - Conception, etape 3 (apres le MCD)
**Version** : v0.2 — prod-like, machine a 4 etats (+ couche security-by-design 2026-06-11)
**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) ; operations security-by-design ajoutees (ERASE_USER_PII, RESET_PASSWORD, ensemble sensible protege par PIN, ecritures audit_log, throttling d'authentification) — 28 operations
**Auteur** : BYAN (couche methodologie)
---
## 1. Purpose
## 1. Objectif
The MCT (Model of Conceptual Treatments) describes the **business operations** of the Wakdo
domain in the canonical Merise form: **triggering event -> operation -> emitted result**.
Le MCT (Modele Conceptuel des Traitements) decrit les **operations metier** du domaine
Wakdo sous la forme canonique Merise : **evenement declencheur -> operation -> resultat emis**.
It answers the question: what happens in the domain, and when?
It does not answer: who does what, on which workstation, in which organisational order
(the MOT level is intentionally skipped — agile shortcut, consistent with the solo RNCP
framework).
Il repond a la question : que se passe-t-il dans le domaine, et quand ?
Il ne repond pas a : qui fait quoi, sur quel poste de travail, dans quel ordre organisationnel
(le niveau MOT est volontairement saute — raccourci agile, coherent avec le cadre RNCP
solo).
The MCT covers:
- The order lifecycle end-to-end (kiosk, counter, drive)
- Catalogue management (manager / admin)
- User and role management (admin)
- Back-office authentication (all back-office actors)
Le MCT couvre :
- Le cycle de vie de la commande de bout en bout (kiosk, comptoir, drive)
- La gestion du catalogue (manager / admin)
- La gestion des utilisateurs et des roles (admin)
- L'authentification back-office (tous les acteurs back-office)
**Identified actors**:
**Acteurs identifies** :
| Actor | Code | Interface |
| Acteur | Code | Interface |
|-------|------|-----------|
| Customer (kiosk) | CUSTOMER | Touch kiosk (public, unauthenticated) |
| Counter staff | COUNTER | Back-office, role `counter` |
| Drive staff | DRIVE | Back-office, role `drive` |
| Kitchen staff | KITCHEN | Back-office, role `kitchen` (read-only on orders) |
| Client (kiosk) | CUSTOMER | Borne tactile (public, non authentifie) |
| Personnel comptoir | COUNTER | Back-office, role `counter` |
| Personnel drive | DRIVE | Back-office, role `drive` |
| Personnel cuisine | KITCHEN | Back-office, role `kitchen` (lecture seule sur les commandes) |
| Manager | MANAGER | Back-office, role `manager` |
| Administrator | ADMIN | Back-office, role `admin` |
| System | SYS | Internal API / PHP logic |
| Administrateur | ADMIN | Back-office, role `admin` |
| Systeme | SYS | API interne / logique PHP |
**MCD cross-reference**: each operation references entities from the MCD (section 14).
The MCT is consistent with the `customer_order.status` state machine:
**Reference croisee MCD** : chaque operation reference des entites du MCD (section 14).
Le MCT est coherent avec la machine a etats de `customer_order.status` :
```
pending_payment -> paid -> delivered
@ -46,32 +46,32 @@ pending_payment -> paid -> delivered
+--------------+-----------> cancelled (from any non-terminal state)
```
**Dropped states** (compared to v0.1): `preparing` and `ready` are removed.
Rationale: in a fast-food context the kitchen display (KDS) is a visual system; staff read
the ticket and act. The single staff gesture is "deliver". KPI is total time
`delivered_at - paid_at` (SLA approx. 10 min). KDS colour coding is computed from
`now - paid_at`; no additional stored state is required.
**Etats supprimes** (par rapport a v0.1) : `preparing` et `ready` sont retires.
Justification : dans un contexte fast-food, l'affichage cuisine (KDS) est un systeme visuel ;
le personnel lit le ticket et agit. L'unique geste du personnel est « delivrer ». Le KPI est
le temps total `delivered_at - paid_at` (SLA approx. 10 min). Le code couleur du KDS est calcule a partir de
`now - paid_at` ; aucun etat stocke supplementaire n'est requis.
**Dropped operations** (compared to v0.1): `MARK_IN_PREPARATION` (`MARQUER_EN_PREPARATION`)
and `MARK_READY` (`MARQUER_PRETE`) are removed because their intermediate states no longer
exist. `DELIVER_ORDER` becomes the sole status-advancing action for counter/drive staff.
**Operations supprimees** (par rapport a v0.1) : `MARK_IN_PREPARATION` (`MARQUER_EN_PREPARATION`)
et `MARK_READY` (`MARQUER_PRETE`) sont retirees car leurs etats intermediaires n'existent plus.
`DELIVER_ORDER` devient la seule action faisant avancer le statut pour le personnel comptoir/drive.
**Security-by-design layer (2026-06-11)**: two operations are added`RESET_PASSWORD` (12.3)
and `ERASE_USER_PII` (10.5, RGPD anonymisation). A subset of operations is **PIN-gated**:
back-office sessions stay shared per workstation, but a per-staff PIN re-authorises the
sensitive set`CANCEL_ORDER` (7.1), `UPDATE_PRODUCT`/`DELETE_PRODUCT` (8.2/8.3),
`DELETE_MENU` (8.6), `INVENTORY_COUNT` (9.2), user management (10.1-10.3), `MANAGE_RBAC`
(10.4), `ERASE_USER_PII` (10.5). These non-stock actions append an immutable `audit_log` row
(actor, action, target); stock actions record attribution in `stock_movement`. The treatment
logic (PIN, audit, throttling, idempotency, atomic stock decrement, computed product
availability) is specified in `mlt.md` (rules RG-T13-T21). This adds entities 20 `audit_log`
and 21 `login_throttle` to the model.
**Couche security-by-design (2026-06-11)** : deux operations sont ajoutees`RESET_PASSWORD` (12.3)
et `ERASE_USER_PII` (10.5, anonymisation RGPD). Un sous-ensemble d'operations est **protege par PIN** :
les sessions back-office restent partagees par poste de travail, mais un PIN par membre du personnel
re-autorise l'ensemble sensible`CANCEL_ORDER` (7.1), `UPDATE_PRODUCT`/`DELETE_PRODUCT` (8.2/8.3),
`DELETE_MENU` (8.6), `INVENTORY_COUNT` (9.2), gestion des utilisateurs (10.1-10.3), `MANAGE_RBAC`
(10.4), `ERASE_USER_PII` (10.5). Ces actions hors stock ajoutent une ligne `audit_log` immuable
(acteur, action, cible) ; les actions de stock enregistrent l'attribution dans `stock_movement`. La logique
de traitement (PIN, audit, throttling, idempotence, decrement atomique du stock, disponibilite produit
calculee) est specifiee dans `mlt.md` (regles RG-T13-T21). Cela ajoute les entites 20 `audit_log`
et 21 `login_throttle` au modele.
---
## 2. Representation conventions
## 2. Conventions de representation
### Operation format
### Format des operations
```
[TRIGGERING EVENT(S)]
@ -84,467 +84,467 @@ and 21 `login_throttle` to the model.
[EMITTED RESULT(S)]
```
**Synchronisations**:
- `AND`: all events must be present simultaneously to trigger the operation.
- `OR`: any one of the events is sufficient.
**Synchronisations** :
- `AND` : tous les evenements doivent etre presents simultanement pour declencher l'operation.
- `OR` : l'un quelconque des evenements suffit.
**Conditions**: expressed in square brackets `[condition]` on the incoming arc.
**Conditions** : exprimees entre crochets `[condition]` sur l'arc entrant.
### Textual notation
### Notation textuelle
For each operation the document provides:
- **Triggering event(s)**: what occurs and causes the operation.
- **Actor(s)**: who initiates (or validates).
- **Synchronisation**: `AND` / `OR` if multiple events, plus condition.
- **Operation**: name and description of what it does.
- **MCD entities touched**: read (R) or write (W).
- **Result(s)**: what is emitted or produced.
Pour chaque operation, le document fournit :
- **Evenement(s) declencheur(s)** : ce qui survient et provoque l'operation.
- **Acteur(s)** : qui initie (ou valide).
- **Synchronisation** : `AND` / `OR` si plusieurs evenements, plus la condition.
- **Operation** : nom et description de ce qu'elle fait.
- **Entites MCD touchees** : lecture (R) ou ecriture (W).
- **Resultat(s)** : ce qui est emis ou produit.
---
## 3. Domain 1 — Order lifecycle (kiosk)
## 3. Domaine 1 — Cycle de vie de la commande (kiosk)
### 3.1 LOAD_CATALOGUE
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Customer opens the kiosk (connection to the kiosk endpoint) |
| **Actor** | CUSTOMER |
| **Synchronisation** | None (single event) |
| **Condition** | The kiosk is in service (within business hours 10:00-01:00) |
| **Evenement declencheur** | Le client ouvre le kiosk (connexion a l'endpoint du kiosk) |
| **Acteur** | CUSTOMER |
| **Synchronisation** | Aucune (evenement unique) |
| **Condition** | Le kiosk est en service (dans les horaires d'ouverture 10:00-01:00) |
| **Operation** | LOAD_CATALOGUE |
| **Description** | Retrieval of active categories, available products, and available menus (with their slots and eligible options) for display on the kiosk screen. Product availability is COMPUTED: a product is orderable only if its `is_available` flag is set AND each non-removable (`is_removable=0`) ingredient in its `product_ingredient` is above the critical band (`stock_quantity > stock_capacity * critical_stock_pct/100`). See rule RG-T21 in `mlt.md`. |
| **MCD entities** | R: `category` (is_active=1), `product` (is_available=1), `menu` (is_available=1), `menu_slot`, `menu_slot_option`, `ingredient` (is_active=1), `allergen`, `ingredient_allergen` |
| **Result** | Catalogue loaded; kiosk displays the home screen |
| **Description** | Recuperation des categories actives, des produits disponibles et des menus disponibles (avec leurs slots et options eligibles) pour affichage sur l'ecran du kiosk. La disponibilite des produits est CALCULEE : un produit est commandable seulement si son flag `is_available` est positionne ET que chaque ingredient non retirable (`is_removable=0`) de son `product_ingredient` est au-dessus de la bande critique (`stock_quantity > stock_capacity * critical_stock_pct/100`). Voir la regle RG-T21 dans `mlt.md`. |
| **Entites MCD** | R: `category` (is_active=1), `product` (is_available=1), `menu` (is_available=1), `menu_slot`, `menu_slot_option`, `ingredient` (is_active=1), `allergen`, `ingredient_allergen` |
| **Resultat** | Catalogue charge ; le kiosk affiche l'ecran d'accueil |
---
### 3.2 COMPOSE_CART
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Customer selects a product or a menu on the kiosk |
| **Actor** | CUSTOMER |
| **Synchronisation** | Repeatable event (OR: add product, add menu, change quantity, remove item, choose menu slot, choose format Normal/Maxi, add/remove ingredient modifier) |
| **Condition** | The selected product or menu has `is_available=1` |
| **Evenement declencheur** | Le client selectionne un produit ou un menu sur le kiosk |
| **Acteur** | CUSTOMER |
| **Synchronisation** | Evenement repetable (OR : ajouter produit, ajouter menu, modifier quantite, retirer un article, choisir un slot de menu, choisir le format Normal/Maxi, ajouter/retirer un modificateur d'ingredient) |
| **Condition** | Le produit ou le menu selectionne a `is_available=1` |
| **Operation** | COMPOSE_CART |
| **Description** | In-memory cart construction: add an item (standalone product or menu), select slot products (`order_item_selection`), optionally modify ingredients (`order_item_modifier`), choose Normal or Maxi format for menus, recalculate TTC total. The cart is a volatile client-side structure; no database write at this stage. |
| **MCD entities** | R: `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `product_ingredient` — W: none (volatile front-end state) |
| **Result** | Cart updated, total recalculated, summary displayed |
| **Description** | Construction du panier en memoire : ajouter un article (produit autonome ou menu), selectionner les produits des slots (`order_item_selection`), modifier optionnellement les ingredients (`order_item_modifier`), choisir le format Normal ou Maxi pour les menus, recalculer le total TTC. Le panier est une structure volatile cote client ; aucune ecriture en base a ce stade. |
| **Entites MCD** | R: `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `product_ingredient` — W: aucune (etat volatile front-end) |
| **Resultat** | Panier mis a jour, total recalcule, recapitulatif affiche |
---
### 3.3 CREATE_ORDER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering events** | 1. Customer confirms cart (presses "Validate") AND 2. Customer enters their order number (RNCP payment substitute) |
| **Actor** | CUSTOMER |
| **Synchronisation** | AND (both actions required) |
| **Condition** | Cart contains at least 1 item. The order number entered is non-empty. |
| **Evenements declencheurs** | 1. Le client confirme le panier (appuie sur « Valider ») AND 2. Le client saisit son numero de commande (substitut de paiement RNCP) |
| **Acteur** | CUSTOMER |
| **Synchronisation** | AND (les deux actions requises) |
| **Condition** | Le panier contient au moins 1 article. Le numero de commande saisi est non vide. |
| **Operation** | CREATE_ORDER |
| **Description** | Atomic order creation: INSERT `customer_order` with status `pending_payment`, source `kiosk`, snapshot of HT/VAT/TTC totals (computed line by line using `vat_rate` snapshotted per item). INSERT `order_item` lines with `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`. INSERT `order_item_selection` for each slot filled in a menu item. INSERT `order_item_modifier` for each ingredient modification. Decrement `ingredient.stock_quantity` for each ingredient consumed (adjusted by modifiers: remove => no decrement; add => extra decrement); INSERT one `stock_movement` row of type `sale` per affected ingredient unit. Stock decrements and order insert are within the same transaction. After the customer enters their order number, the status transitions `pending_payment -> paid` within the same transaction; `paid_at` is set. The system generates the order number in format `K-YYYY-MM-DD-NNN`. |
| **MCD entities** | 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) |
| **Result** | Order created (status `paid` at end of operation), order number displayed to customer, logical event ORDER_CREATED emitted toward the preparation domain |
| **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`. |
| **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 |
---
### 3.4 DISPLAY_CONFIRMATION
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | ORDER_CREATED (API response 201 after CREATE_ORDER) |
| **Actor** | SYS |
| **Synchronisation** | None |
| **Condition** | API response contains an id, an order_number and status `paid` |
| **Evenement declencheur** | ORDER_CREATED (reponse API 201 apres CREATE_ORDER) |
| **Acteur** | SYS |
| **Synchronisation** | Aucune |
| **Condition** | La reponse API contient un id, un order_number et le statut `paid` |
| **Operation** | DISPLAY_CONFIRMATION |
| **Description** | Display of the confirmation screen on the kiosk with the order number. The kiosk then resets for the next customer. |
| **MCD entities** | R: none (data is in the API response) |
| **Result** | Confirmation screen displayed; kiosk available for next order |
| **Description** | Affichage de l'ecran de confirmation sur le kiosk avec le numero de commande. Le kiosk se reinitialise ensuite pour le client suivant. |
| **Entites MCD** | R: aucune (les donnees sont dans la reponse API) |
| **Resultat** | Ecran de confirmation affiche ; kiosk disponible pour la commande suivante |
---
## 4. Domain 2 — Order lifecycle (counter and drive)
## 4. Domaine 2 — Cycle de vie de la commande (comptoir et drive)
### 4.1 CREATE_COUNTER_ORDER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | A counter or drive staff member initiates a new order from the back-office |
| **Actor** | COUNTER or DRIVE |
| **Synchronisation** | None |
| **Condition** | The actor is authenticated and holds permission `order.create`. The `source` is `counter` or `drive` (auto-tagged from `role.order_source`). |
| **Evenement declencheur** | Un membre du personnel comptoir ou drive initie une nouvelle commande depuis le back-office |
| **Acteur** | COUNTER ou DRIVE |
| **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`). |
| **Operation** | CREATE_COUNTER_ORDER |
| **Description** | Manual order composition via the back-office: select products and menus, choose service mode (`dine_in`/`takeaway`/`drive`), fill menu slots, add ingredient modifiers. Identical creation logic to CREATE_ORDER (snapshot, stock decrement in same transaction, atomic `pending_payment -> paid` transition). The `source` is auto-tagged from `role.order_source` (counter -> `counter`, drive -> `drive`). Order number format: `C-YYYY-MM-DD-NNN` (counter) or `D-YYYY-MM-DD-NNN` (drive). Cross-constraint: if `source = 'drive'` then `service_mode = 'drive'` (verified at creation). |
| **MCD entities** | 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`) |
| **Result** | Order created (status `paid`), order number communicated to customer |
| **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). |
| **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 |
---
## 5. Domain 3 — Preparation display (kitchen)
## 5. Domaine 3 — Affichage de preparation (cuisine)
### 5.1 LIST_ORDERS_DISPLAY
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Kitchen staff accesses or refreshes the preparation display |
| **Actor** | KITCHEN (or COUNTER, DRIVE, ADMIN) |
| **Synchronisation** | None |
| **Condition** | The actor is authenticated and holds permission `order.read`. |
| **Evenement declencheur** | Le personnel cuisine accede a l'affichage de preparation ou le rafraichit |
| **Acteur** | KITCHEN (ou COUNTER, DRIVE, ADMIN) |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur est authentifie et detient la permission `order.read`. |
| **Operation** | LIST_ORDERS_DISPLAY |
| **Description** | Read `customer_order` rows with status `paid`, filtered by sources visible to the actor's role (from `role_visible_source`): kitchen sees all sources; counter sees kiosk+counter; drive sees drive. Orders are sorted by `paid_at` ascending (oldest first). For each order, display: order number, source, content (`order_item` with `label_snapshot`, `quantity`, format, slot selections, ingredient modifiers). KDS colour is computed from `now - paid_at` against the SLA threshold (approx. 10 min), not stored. Kitchen staff performs no status transition — this is a read-only operation. |
| **MCD entities** | R: `customer_order` (status=`paid`), `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` |
| **Result** | Preparation display list shown, sorted by payment time ascending |
| **Description** | Lecture des lignes `customer_order` avec statut `paid`, filtrees par les sources visibles selon le role de l'acteur (depuis `role_visible_source`) : la cuisine voit toutes les sources ; le comptoir voit kiosk+counter ; le drive voit drive. Les commandes sont triees par `paid_at` ascendant (les plus anciennes en premier). Pour chaque commande, afficher : numero de commande, source, contenu (`order_item` avec `label_snapshot`, `quantity`, format, selections de slots, modificateurs d'ingredient). La couleur KDS est calculee a partir de `now - paid_at` par rapport au seuil de SLA (approx. 10 min), non stockee. Le personnel cuisine n'effectue aucune transition de statut — c'est une operation en lecture seule. |
| **Entites MCD** | R: `customer_order` (status=`paid`), `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` |
| **Resultat** | Liste d'affichage de preparation montree, triee par heure de paiement ascendante |
---
## 6. Domain 4 — Delivery to customer
## 6. Domaine 4 — Livraison au client
### 6.1 DELIVER_ORDER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering events** | 1. The order is at status `paid` AND 2. Counter or drive staff clicks "Delivered" |
| **Actor** | COUNTER or DRIVE |
| **Evenements declencheurs** | 1. La commande est au statut `paid` AND 2. Le personnel comptoir ou drive clique sur « Livre » |
| **Acteur** | COUNTER ou DRIVE |
| **Synchronisation** | AND |
| **Condition** | The order has status `paid`. The actor holds permission `order.deliver`. The actor's role is consistent with the order source (counter staff handles kiosk+counter orders; drive staff handles drive orders — filtered by role_visible_source). |
| **Condition** | La commande a le statut `paid`. L'acteur detient la permission `order.deliver`. Le role de l'acteur est coherent avec la source de la commande (le personnel comptoir traite les commandes kiosk+counter ; le personnel drive traite les commandes drive — filtre par role_visible_source). |
| **Operation** | DELIVER_ORDER |
| **Description** | Single-gesture transition `paid -> delivered`. Sets `delivered_at = NOW()`. The order moves to history. This operation replaces the v0.1 two-step sequence (mark-ready then deliver); the kitchen's visual confirmation (KDS) is sufficient before this action. |
| **MCD entities** | W: `customer_order` (UPDATE status `paid` -> `delivered`, `delivered_at = NOW()`) |
| **Result** | Order at status `delivered`, lifecycle complete |
| **Description** | Transition en geste unique `paid -> delivered`. Positionne `delivered_at = NOW()`. La commande passe en historique. Cette operation remplace la sequence en deux etapes de v0.1 (marquer-prete puis livrer) ; la confirmation visuelle de la cuisine (KDS) suffit avant cette action. |
| **Entites MCD** | W: `customer_order` (UPDATE status `paid` -> `delivered`, `delivered_at = NOW()`) |
| **Resultat** | Commande au statut `delivered`, cycle de vie complet |
---
## 7. Domain 5 — Cancellation
## 7. Domaine 5 — Annulation
### 7.1 CANCEL_ORDER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | An authorised actor requests cancellation of an order |
| **Actor** | COUNTER, DRIVE, or ADMIN |
| **Synchronisation** | None |
| **Condition** | The order exists. `customer_order.status` is in `['pending_payment', 'paid']`. Terminal statuses `delivered` and `cancelled` cannot transition to `cancelled`. The actor holds permission `order.cancel`. |
| **Evenement declencheur** | Un acteur autorise demande l'annulation d'une commande |
| **Acteur** | COUNTER, DRIVE ou ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | La commande existe. `customer_order.status` est dans `['pending_payment', 'paid']`. Les statuts terminaux `delivered` et `cancelled` ne peuvent pas transiter vers `cancelled`. L'acteur detient la permission `order.cancel`. |
| **Operation** | CANCEL_ORDER |
| **Description** | Transition from current status to `cancelled`. Sets `cancelled_at = NOW()`. The order is retained in the database for history and stats (no physical deletion). If the current status is `paid`, stock is re-credited: for each ingredient consumed by the order (accounting for modifiers), `ingredient.stock_quantity` is incremented; one `stock_movement` row of type `cancellation` is inserted per affected ingredient unit. Stock re-credit and status update are within the same transaction. |
| **MCD entities** | R: `order_item`, `order_item_modifier`, `ingredient`, `product_ingredient` — W: `customer_order` (UPDATE status -> `cancelled`, `cancelled_at = NOW()`), `ingredient` (UPDATE stock_quantity, conditional on status `paid`), `stock_movement` (INSERT type `cancellation`, conditional on status `paid`) |
| **Result** | Order at status `cancelled`, visible in admin history |
| **Description** | Transition du statut courant vers `cancelled`. Positionne `cancelled_at = NOW()`. La commande est conservee en base pour l'historique et les stats (pas de suppression physique). Si le statut courant est `paid`, le stock est recredite : pour chaque ingredient consomme par la commande (en tenant compte des modificateurs), `ingredient.stock_quantity` est incremente ; une ligne `stock_movement` de type `cancellation` est inseree par unite d'ingredient affectee. Le recredit du stock et la mise a jour du statut sont dans la meme transaction. |
| **Entites MCD** | R: `order_item`, `order_item_modifier`, `ingredient`, `product_ingredient` — W: `customer_order` (UPDATE status -> `cancelled`, `cancelled_at = NOW()`), `ingredient` (UPDATE stock_quantity, conditional on status `paid`), `stock_movement` (INSERT type `cancellation`, conditional on status `paid`) |
| **Resultat** | Commande au statut `cancelled`, visible dans l'historique admin |
---
## 8. Domain 6 — Catalogue management
## 8. Domaine 6 — Gestion du catalogue
### 8.1 CREATE_PRODUCT
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin or manager submits the product creation form |
| **Actor** | ADMIN or MANAGER |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `product.create`. Target category exists and `is_active=1`. `name` is non-empty. `price_cents > 0`. |
| **Evenement declencheur** | L'admin ou le manager soumet le formulaire de creation de produit |
| **Acteur** | ADMIN ou MANAGER |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `product.create`. La categorie cible existe et `is_active=1`. `name` est non vide. `price_cents > 0`. |
| **Operation** | CREATE_PRODUCT |
| **Description** | INSERT a new `product` with its category, name, price in cents, VAT rate in per-mille (`vat_rate`: 100=10%, 55=5.5%, default 100), optional image path. `is_available=1` by default. |
| **MCD entities** | R: `category` (FK validation) — W: `product` (INSERT) |
| **Result** | Product created, redirect to product list |
| **Description** | INSERT d'un nouveau `product` avec sa categorie, son nom, son prix en centimes, son taux de TVA en pour-mille (`vat_rate` : 100=10%, 55=5.5%, defaut 100), chemin d'image optionnel. `is_available=1` par defaut. |
| **Entites MCD** | R: `category` (FK validation) — W: `product` (INSERT) |
| **Resultat** | Produit cree, redirection vers la liste des produits |
---
### 8.2 UPDATE_PRODUCT
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin or manager submits the product update form |
| **Actor** | ADMIN or MANAGER |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `product.update`. Product exists. New values respect constraints (`price_cents > 0`, non-empty name). |
| **Evenement declencheur** | L'admin ou le manager soumet le formulaire de modification de produit |
| **Acteur** | ADMIN ou MANAGER |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `product.update`. Le produit existe. Les nouvelles valeurs respectent les contraintes (`price_cents > 0`, nom non vide). |
| **Operation** | UPDATE_PRODUCT |
| **Description** | UPDATE modifiable columns (`name`, `description`, `price_cents`, `vat_rate`, `image_path`, `is_available`, `display_order`, `category_id`). Snapshots already stored in `order_item` are not affected (historical integrity guaranteed by design). |
| **MCD entities** | W: `product` (UPDATE) |
| **Result** | Product updated, product list refreshed |
| **Description** | UPDATE des colonnes modifiables (`name`, `description`, `price_cents`, `vat_rate`, `image_path`, `is_available`, `display_order`, `category_id`). Les snapshots deja stockes dans `order_item` ne sont pas affectes (integrite historique garantie par conception). |
| **Entites MCD** | W: `product` (UPDATE) |
| **Resultat** | Produit mis a jour, liste des produits rafraichie |
---
### 8.3 DELETE_PRODUCT
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin confirms deletion of a product |
| **Actor** | ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `product.delete`. Product is not a slot option in any `menu_slot_option` (FK `ON DELETE RESTRICT`). Product is not referenced in any `order_item` historical line (FK `ON DELETE RESTRICT`). Preliminary check required. |
| **Evenement declencheur** | L'admin confirme la suppression d'un produit |
| **Acteur** | ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `product.delete`. Le produit n'est option de slot dans aucun `menu_slot_option` (FK `ON DELETE RESTRICT`). Le produit n'est reference dans aucune ligne historique `order_item` (FK `ON DELETE RESTRICT`). Verification prealable requise. |
| **Operation** | DELETE_PRODUCT |
| **Description** | Physical deletion of the product if no FK constraint blocks. If the product is referenced in a menu slot or historical order line, deletion is blocked. The recommended alternative is to deactivate (`is_available=0`). Also blocks if the product is the `burger_product_id` of any `menu`. |
| **MCD entities** | W: `product` (DELETE — blocked if referenced in `menu_slot_option`, `order_item`, or `menu.burger_product_id`) |
| **Result** | Product deleted OR error "product in use" |
| **Description** | Suppression physique du produit si aucune contrainte FK ne la bloque. Si le produit est reference dans un slot de menu ou une ligne de commande historique, la suppression est bloquee. L'alternative recommandee est de le desactiver (`is_available=0`). Bloque egalement si le produit est le `burger_product_id` d'un `menu`. |
| **Entites MCD** | W: `product` (DELETE — blocked if referenced in `menu_slot_option`, `order_item`, or `menu.burger_product_id`) |
| **Resultat** | Produit supprime OU erreur « produit utilise » |
---
### 8.4 CREATE_MENU
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin or manager submits the menu creation form with its slot configuration |
| **Actor** | ADMIN or MANAGER |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `menu.create`. `name` is non-empty. `price_normal_cents > 0`, `price_maxi_cents > 0`. `burger_product_id` references an existing product. At least one slot is defined with at least one option. |
| **Evenement declencheur** | L'admin ou le manager soumet le formulaire de creation de menu avec sa configuration de slots |
| **Acteur** | ADMIN ou MANAGER |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `menu.create`. `name` est non vide. `price_normal_cents > 0`, `price_maxi_cents > 0`. `burger_product_id` reference un produit existant. Au moins un slot est defini avec au moins une option. |
| **Operation** | CREATE_MENU |
| **Description** | Transaction: INSERT `menu` (with `burger_product_id`, `price_normal_cents`, `price_maxi_cents`), then INSERT `menu_slot` rows (one per slot: drink, side, sauce...), then INSERT `menu_slot_option` rows (eligible products per slot). |
| **MCD entities** | R: `product` (burger FK validation, slot options validation), `category` — W: `menu` (INSERT), `menu_slot` (INSERT), `menu_slot_option` (INSERT) |
| **Result** | Menu created with its slot configuration, visible on the kiosk |
| **Description** | Transaction : INSERT `menu` (avec `burger_product_id`, `price_normal_cents`, `price_maxi_cents`), puis INSERT des lignes `menu_slot` (une par slot : boisson, accompagnement, sauce...), puis INSERT des lignes `menu_slot_option` (produits eligibles par slot). |
| **Entites MCD** | R: `product` (burger FK validation, slot options validation), `category` — W: `menu` (INSERT), `menu_slot` (INSERT), `menu_slot_option` (INSERT) |
| **Resultat** | Menu cree avec sa configuration de slots, visible sur le kiosk |
---
### 8.5 UPDATE_MENU
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin or manager submits the menu update form |
| **Actor** | ADMIN or MANAGER |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `menu.update`. Menu exists. Updated configuration preserves at least one slot with at least one option. |
| **Evenement declencheur** | L'admin ou le manager soumet le formulaire de modification de menu |
| **Acteur** | ADMIN ou MANAGER |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `menu.update`. Le menu existe. La configuration mise a jour preserve au moins un slot avec au moins une option. |
| **Operation** | UPDATE_MENU |
| **Description** | UPDATE `menu` columns. If slot configuration is modified: DELETE all `menu_slot_option` rows for this menu's slots, DELETE `menu_slot` rows, then re-INSERT (delete-and-reinsert pattern, atomic in transaction). Snapshots in `order_item` are not affected. |
| **MCD entities** | W: `menu` (UPDATE), `menu_slot` (DELETE + INSERT), `menu_slot_option` (DELETE + INSERT) |
| **Result** | Menu updated |
| **Description** | UPDATE des colonnes `menu`. Si la configuration des slots est modifiee : DELETE de toutes les lignes `menu_slot_option` pour les slots de ce menu, DELETE des lignes `menu_slot`, puis re-INSERT (pattern delete-and-reinsert, atomique en transaction). Les snapshots dans `order_item` ne sont pas affectes. |
| **Entites MCD** | W: `menu` (UPDATE), `menu_slot` (DELETE + INSERT), `menu_slot_option` (DELETE + INSERT) |
| **Resultat** | Menu mis a jour |
---
### 8.6 DELETE_MENU
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin confirms deletion of a menu |
| **Actor** | ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `menu.delete`. Menu is not referenced in any `order_item` historical line (FK `ON DELETE RESTRICT`). Preliminary check required. |
| **Evenement declencheur** | L'admin confirme la suppression d'un menu |
| **Acteur** | ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `menu.delete`. Le menu n'est reference dans aucune ligne historique `order_item` (FK `ON DELETE RESTRICT`). Verification prealable requise. |
| **Operation** | DELETE_MENU |
| **Description** | If no `order_item` references this menu: DELETE `menu_slot_option` (CASCADE from `menu_slot`), DELETE `menu_slot` (CASCADE from `menu`), DELETE `menu`. If historical references exist, propose deactivation (`is_available=0`) instead. |
| **MCD entities** | W: `menu_slot_option` (DELETE CASCADE), `menu_slot` (DELETE CASCADE), `menu` (DELETE — blocked if referenced in `order_item`) |
| **Result** | Menu deleted OR error "menu present in historical orders" |
| **Description** | Si aucun `order_item` ne reference ce menu : DELETE `menu_slot_option` (CASCADE from `menu_slot`), DELETE `menu_slot` (CASCADE from `menu`), DELETE `menu`. Si des references historiques existent, proposer la desactivation (`is_available=0`) a la place. |
| **Entites MCD** | W: `menu_slot_option` (DELETE CASCADE), `menu_slot` (DELETE CASCADE), `menu` (DELETE — blocked if referenced in `order_item`) |
| **Resultat** | Menu supprime OU erreur « menu present dans des commandes historiques » |
---
### 8.7 MANAGE_CATEGORY
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin or manager creates, updates, or deactivates a category |
| **Actor** | ADMIN or MANAGER |
| **Synchronisation** | OR (create, update, deactivation) |
| **Condition** | Actor holds permission `category.manage`. For deactivation: products and menus in the category are not auto-deactivated in DB (no CASCADE on `is_active`); the application layer proposes deactivating child products/menus. |
| **Evenement declencheur** | L'admin ou le manager cree, modifie ou desactive une categorie |
| **Acteur** | ADMIN ou MANAGER |
| **Synchronisation** | OR (creation, modification, desactivation) |
| **Condition** | L'acteur detient la permission `category.manage`. Pour la desactivation : les produits et menus de la categorie ne sont pas auto-desactives en base (pas de CASCADE sur `is_active`) ; la couche applicative propose de desactiver les produits/menus enfants. |
| **Operation** | MANAGE_CATEGORY |
| **Description** | CRUD on `category`. Deactivation (`is_active=0`) hides the category and its products from the kiosk without physical deletion. Physical deletion is blocked if products or menus reference this category (FK `ON DELETE RESTRICT`). |
| **MCD entities** | W: `category` (INSERT / UPDATE / conditional DELETE) |
| **Result** | Category created / updated / deactivated |
| **Description** | CRUD sur `category`. La desactivation (`is_active=0`) masque la categorie et ses produits du kiosk sans suppression physique. La suppression physique est bloquee si des produits ou des menus referencent cette categorie (FK `ON DELETE RESTRICT`). |
| **Entites MCD** | W: `category` (INSERT / UPDATE / conditional DELETE) |
| **Resultat** | Categorie creee / modifiee / desactivee |
---
### 8.8 MANAGE_INGREDIENT
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin or manager creates, updates, or deactivates an ingredient; or manages product composition (`product_ingredient`) or allergen mapping (`ingredient_allergen`) |
| **Actor** | ADMIN or MANAGER |
| **Synchronisation** | OR (create ingredient, update ingredient, update composition, update allergen mapping) |
| **Condition** | Actor holds permission `ingredient.manage`. |
| **Evenement declencheur** | L'admin ou le manager cree, modifie ou desactive un ingredient ; ou gere la composition produit (`product_ingredient`) ou le mapping allergene (`ingredient_allergen`) |
| **Acteur** | ADMIN ou MANAGER |
| **Synchronisation** | OR (creer ingredient, modifier ingredient, modifier composition, modifier mapping allergene) |
| **Condition** | L'acteur detient la permission `ingredient.manage`. |
| **Operation** | MANAGE_INGREDIENT |
| **Description** | CRUD on `ingredient` (name, unit, pack_size, pack_label, stock_capacity, low_stock_pct, critical_stock_pct, is_active). Manage `product_ingredient` composition (quantity_normal, quantity_maxi, is_removable, is_addable, extra_price_cents) for any product. Manage `ingredient_allergen` mapping (14 EU regulated allergens). Deactivating an ingredient (`is_active=0`) hides it from the configurator without deletion. Physical deletion of `ingredient` is blocked if referenced in `product_ingredient` (FK `ON DELETE RESTRICT`) or `stock_movement` (FK `ON DELETE RESTRICT`). |
| **MCD entities** | R: `product` (FK validation), `allergen` (FK validation) — W: `ingredient` (INSERT/UPDATE/DELETE conditional), `product_ingredient` (INSERT/UPDATE/DELETE), `ingredient_allergen` (INSERT/DELETE) |
| **Result** | Ingredient / composition / allergen mapping updated |
| **Description** | CRUD sur `ingredient` (name, unit, pack_size, pack_label, stock_capacity, low_stock_pct, critical_stock_pct, is_active). Gestion de la composition `product_ingredient` (quantity_normal, quantity_maxi, is_removable, is_addable, extra_price_cents) pour tout produit. Gestion du mapping `ingredient_allergen` (14 allergenes reglementes UE). Desactiver un ingredient (`is_active=0`) le masque du configurateur sans suppression. La suppression physique de `ingredient` est bloquee s'il est reference dans `product_ingredient` (FK `ON DELETE RESTRICT`) ou `stock_movement` (FK `ON DELETE RESTRICT`). |
| **Entites MCD** | R: `product` (FK validation), `allergen` (FK validation) — W: `ingredient` (INSERT/UPDATE/DELETE conditional), `product_ingredient` (INSERT/UPDATE/DELETE), `ingredient_allergen` (INSERT/DELETE) |
| **Resultat** | Ingredient / composition / mapping allergene mis a jour |
---
## 9. Domain 7 — Stock management
## 9. Domaine 7 — Gestion du stock
### 9.1 RESTOCK
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Manager or admin records a delivery of ingredient packs |
| **Actor** | MANAGER or ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `stock.manage`. Ingredient exists and `is_active=1`. Number of packs `N >= 1`. |
| **Evenement declencheur** | Le manager ou l'admin enregistre une livraison de packs d'ingredient |
| **Acteur** | MANAGER ou ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `stock.manage`. L'ingredient existe et `is_active=1`. Nombre de packs `N >= 1`. |
| **Operation** | RESTOCK |
| **Description** | UPDATE `ingredient.stock_quantity += N * pack_size`. INSERT one `stock_movement` row: type `restock`, delta `+= N * pack_size`, `user_id` of the actor, optional `note` (e.g. delivery reference). Both writes are in the same transaction. |
| **MCD entities** | R: `ingredient` — W: `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `restock`) |
| **Result** | Stock incremented, movement logged |
| **Description** | UPDATE `ingredient.stock_quantity += N * pack_size`. INSERT d'une ligne `stock_movement` : type `restock`, delta `+= N * pack_size`, `user_id` de l'acteur, `note` optionnelle (ex. reference de livraison). Les deux ecritures sont dans la meme transaction. |
| **Entites MCD** | R: `ingredient` — W: `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `restock`) |
| **Resultat** | Stock incremente, mouvement journalise |
---
### 9.2 INVENTORY_COUNT
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | A staff member or manager records the result of a physical inventory count |
| **Actor** | KITCHEN, COUNTER, DRIVE, MANAGER, or ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `stock.count`. Ingredient exists. Physical count `actual_quantity >= 0`. |
| **Evenement declencheur** | Un membre du personnel ou un manager enregistre le resultat d'un inventaire physique |
| **Acteur** | KITCHEN, COUNTER, DRIVE, MANAGER ou ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `stock.count`. L'ingredient existe. Comptage physique `actual_quantity >= 0`. |
| **Operation** | INVENTORY_COUNT |
| **Description** | Compute `delta = actual_quantity - ingredient.stock_quantity` (may be negative or positive). UPDATE `ingredient.stock_quantity = actual_quantity`. INSERT one `stock_movement` row: type `inventory_correction`, delta = computed discrepancy, `user_id` of the actor, optional `note`. Both writes in the same transaction. |
| **MCD entities** | R: `ingredient` (read current stock_quantity) — W: `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `inventory_correction`) |
| **Result** | Stock reconciled to physical count, discrepancy logged |
| **Description** | Calcul de `delta = actual_quantity - ingredient.stock_quantity` (peut etre negatif ou positif). UPDATE `ingredient.stock_quantity = actual_quantity`. INSERT d'une ligne `stock_movement` : type `inventory_correction`, delta = ecart calcule, `user_id` de l'acteur, `note` optionnelle. Les deux ecritures dans la meme transaction. |
| **Entites MCD** | R: `ingredient` (read current stock_quantity) — W: `ingredient` (UPDATE stock_quantity), `stock_movement` (INSERT type `inventory_correction`) |
| **Resultat** | Stock reconcilie au comptage physique, ecart journalise |
---
### 9.3 READ_STOCK
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | An authorised actor accesses the stock view |
| **Actor** | KITCHEN, COUNTER, DRIVE, MANAGER, or ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `stock.read`. |
| **Evenement declencheur** | Un acteur autorise accede a la vue du stock |
| **Acteur** | KITCHEN, COUNTER, DRIVE, MANAGER ou ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `stock.read`. |
| **Operation** | READ_STOCK |
| **Description** | Read `ingredient` list with current `stock_quantity`, `stock_capacity`, computed `stock_pct`, `low_stock_pct`, `critical_stock_pct`, `pack_size`, `pack_label`. Stock bands computed at display time: `low_stock` when `stock_quantity <= stock_capacity * low_stock_pct/100`, `critical_stock` when `stock_quantity <= stock_capacity * critical_stock_pct/100`. Optional: read `stock_movement` history for a given ingredient, filtered by date range. |
| **MCD entities** | R: `ingredient`, `stock_movement` (optional history) |
| **Result** | Stock list displayed with low-stock indicators |
| **Description** | Lecture de la liste `ingredient` avec le `stock_quantity` courant, `stock_capacity`, `stock_pct` calcule, `low_stock_pct`, `critical_stock_pct`, `pack_size`, `pack_label`. Bandes de stock calculees au moment de l'affichage : `low_stock` lorsque `stock_quantity <= stock_capacity * low_stock_pct/100`, `critical_stock` lorsque `stock_quantity <= stock_capacity * critical_stock_pct/100`. Optionnel : lecture de l'historique `stock_movement` pour un ingredient donne, filtre par plage de dates. |
| **Entites MCD** | R: `ingredient`, `stock_movement` (optional history) |
| **Resultat** | Liste du stock affichee avec indicateurs de stock bas |
---
## 10. Domain 8 — User and role management (admin)
## 10. Domaine 8 — Gestion des utilisateurs et des roles (admin)
### 10.1 CREATE_USER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin submits the user creation form |
| **Actor** | ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `user.create`. Email does not already exist in `user.email` (UNIQUE constraint). A valid and active `role_id` is selected. |
| **Evenement declencheur** | L'admin soumet le formulaire de creation d'utilisateur |
| **Acteur** | ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `user.create`. L'email n'existe pas deja dans `user.email` (contrainte UNIQUE). Un `role_id` valide et actif est selectionne. |
| **Operation** | CREATE_USER |
| **Description** | INSERT user with argon2id password hash. Email is unique. `role_id` is mandatory (FK NOT NULL). `is_active=1` by default. `last_login_at=NULL` at creation. |
| **MCD entities** | R: `role` (FK validation) — W: `user` (INSERT) |
| **Result** | User created, can log into the back-office |
| **Description** | INSERT de l'utilisateur avec un hash de mot de passe argon2id. L'email est unique. `role_id` est obligatoire (FK NOT NULL). `is_active=1` par defaut. `last_login_at=NULL` a la creation. |
| **Entites MCD** | R: `role` (FK validation) — W: `user` (INSERT) |
| **Resultat** | Utilisateur cree, peut se connecter au back-office |
---
### 10.2 UPDATE_USER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin submits the user update form |
| **Actor** | ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `user.update`. User exists. If a new password is provided, it is re-hashed. |
| **Evenement declencheur** | L'admin soumet le formulaire de modification d'utilisateur |
| **Acteur** | ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `user.update`. L'utilisateur existe. Si un nouveau mot de passe est fourni, il est re-hashe. |
| **Operation** | UPDATE_USER |
| **Description** | UPDATE modifiable fields (`first_name`, `last_name`, `email`, `role_id`, `is_active`). If a new password is supplied, it replaces the existing hash (argon2id rehash). |
| **MCD entities** | W: `user` (UPDATE) |
| **Result** | User updated |
| **Description** | UPDATE des champs modifiables (`first_name`, `last_name`, `email`, `role_id`, `is_active`). Si un nouveau mot de passe est fourni, il remplace le hash existant (rehash argon2id). |
| **Entites MCD** | W: `user` (UPDATE) |
| **Resultat** | Utilisateur mis a jour |
---
### 10.3 DEACTIVATE_USER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin clicks "Deactivate" for a user |
| **Actor** | ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `user.deactivate`. Admin cannot deactivate their own account (application-level protection). |
| **Evenement declencheur** | L'admin clique sur « Desactiver » pour un utilisateur |
| **Acteur** | ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `user.deactivate`. L'admin ne peut pas desactiver son propre compte (protection au niveau applicatif). |
| **Operation** | DEACTIVATE_USER |
| **Description** | UPDATE `is_active=0`. The user's active session is invalidated on next access (middleware checks `is_active=1` on each authenticated request). User is not deleted; history remains traceable. |
| **MCD entities** | W: `user` (UPDATE is_active=0) |
| **Result** | User deactivated, back-office access blocked |
| **Description** | UPDATE `is_active=0`. La session active de l'utilisateur est invalidee au prochain acces (le middleware verifie `is_active=1` a chaque requete authentifiee). L'utilisateur n'est pas supprime ; l'historique reste tracable. |
| **Entites MCD** | W: `user` (UPDATE is_active=0) |
| **Resultat** | Utilisateur desactive, acces back-office bloque |
---
### 10.4 MANAGE_RBAC
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Admin modifies permission assignments for a role, or creates / updates a custom role |
| **Actor** | ADMIN |
| **Synchronisation** | OR (update role permissions, create custom role, update role attributes) |
| **Condition** | Actor holds permission `role.manage`. Selected permissions exist in the `permission` catalogue. |
| **Evenement declencheur** | L'admin modifie les affectations de permissions pour un role, ou cree / modifie un role personnalise |
| **Acteur** | ADMIN |
| **Synchronisation** | OR (modifier les permissions du role, creer un role personnalise, modifier les attributs du role) |
| **Condition** | L'acteur detient la permission `role.manage`. Les permissions selectionnees existent dans le catalogue `permission`. |
| **Operation** | MANAGE_RBAC |
| **Description** | Update `role_permission` for a given role: DELETE existing assignments, INSERT new ones (delete-and-reinsert, atomic in transaction). Permissions themselves are static (declared in migration, not modifiable via UI). Also covers: CREATE/UPDATE custom `role` (code, label, description, default_route, order_source), UPDATE `role_visible_source` (visible dashboard sources for the role). RBAC architecture rule: application code tests permissions, not role names — adding a new role with correct permissions requires no code change. |
| **MCD entities** | R: `role`, `permission` — W: `role_permission` (DELETE + INSERT), `role` (INSERT/UPDATE for custom roles), `role_visible_source` (INSERT/DELETE) |
| **Result** | RBAC matrix updated, effective immediately for new requests of users bearing this role |
| **Description** | Mise a jour de `role_permission` pour un role donne : DELETE des affectations existantes, INSERT des nouvelles (delete-and-reinsert, atomique en transaction). Les permissions elles-memes sont statiques (declarees en migration, non modifiables via l'UI). Couvre egalement : CREATE/UPDATE d'un `role` personnalise (code, label, description, default_route, order_source), UPDATE de `role_visible_source` (sources de tableau de bord visibles pour le role). Regle d'architecture RBAC : 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. |
| **Entites MCD** | R: `role`, `permission` — W: `role_permission` (DELETE + INSERT), `role` (INSERT/UPDATE for custom roles), `role_visible_source` (INSERT/DELETE) |
| **Resultat** | Matrice RBAC mise a jour, effective immediatement pour les nouvelles requetes des utilisateurs porteurs de ce role |
---
### 10.5 ERASE_USER_PII (security-by-design)
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | A RGPD erasure request is processed for a back-office user |
| **Actor** | ADMIN (PIN-gated) |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `user.update` and has re-authorised via PIN. Target user exists and is not already anonymised. |
| **Evenement declencheur** | Une demande d'effacement RGPD est traitee pour un utilisateur back-office |
| **Acteur** | ADMIN (protege par PIN) |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `user.update` et s'est re-autorise via PIN. L'utilisateur cible existe et n'est pas deja anonymise. |
| **Operation** | ERASE_USER_PII |
| **Description** | RGPD right-to-erasure honoured by **anonymisation**, not physical deletion: PII (`email`, `first_name`, `last_name`) is cleared/replaced by a non-identifying placeholder, credentials invalidated, `anonymized_at` set. The row persists so referential links (`stock_movement`, `customer_order`, `audit_log`) stay valid and resolve to an anonymised principal. See `mlt.md` 10.5 and dictionary note 13. |
| **MCD entities** | W: `user` (UPDATE — PII cleared, `anonymized_at` set), `audit_log` (INSERT) |
| **Result** | User anonymised; PII removed; accountability links preserved; one `audit_log` row recorded |
| **Description** | Le droit a l'effacement RGPD est honore par **anonymisation**, non par suppression physique : les PII (`email`, `first_name`, `last_name`) sont effacees/remplacees par un placeholder non identifiant, les identifiants invalides, `anonymized_at` positionne. La ligne persiste afin que les liens referentiels (`stock_movement`, `customer_order`, `audit_log`) restent valides et se resolvent vers un principal anonymise. Voir `mlt.md` 10.5 et la note 13 du dictionnaire. |
| **Entites MCD** | W: `user` (UPDATE — PII cleared, `anonymized_at` set), `audit_log` (INSERT) |
| **Resultat** | Utilisateur anonymise ; PII supprimees ; liens d'imputabilite preserves ; une ligne `audit_log` enregistree |
---
## 11. Domain 9 — Stats and KPI
## 11. Domaine 9 — Stats et KPI
### 11.1 READ_STATS
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Manager or admin accesses the stats dashboard |
| **Actor** | MANAGER or ADMIN |
| **Synchronisation** | None |
| **Condition** | Actor holds permission `stats.read`. |
| **Evenement declencheur** | Le manager ou l'admin accede au tableau de bord des stats |
| **Acteur** | MANAGER ou ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | L'acteur detient la permission `stats.read`. |
| **Operation** | READ_STATS |
| **Description** | Aggregate queries on `customer_order` and `order_item`. Key aggregations: order count and revenue (TTC) by `service_day` (computed with CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END; cutoff at 10:00); top products by `label_snapshot` COUNT in `order_item`; cancellation rate; average delivery time `delivered_at - paid_at`; breakdown by `source` and `service_mode`. Queries exclude cancelled orders from revenue sums but include them in volume counts. No additional stored column for `service_day`; computation at query time. |
| **MCD entities** | R: `customer_order`, `order_item` |
| **Result** | Stats dashboard displayed |
| **Description** | Requetes d'agregation sur `customer_order` et `order_item`. Agregations cles : nombre de commandes et chiffre d'affaires (TTC) par `service_day` (calcule avec CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END ; coupure a 10:00) ; top produits par COUNT de `label_snapshot` dans `order_item` ; taux d'annulation ; temps de livraison moyen `delivered_at - paid_at` ; ventilation par `source` et `service_mode`. Les requetes excluent les commandes annulees des sommes de chiffre d'affaires mais les incluent dans les comptages de volume. Pas de colonne stockee supplementaire pour `service_day` ; calcul au moment de la requete. |
| **Entites MCD** | R: `customer_order`, `order_item` |
| **Resultat** | Tableau de bord des stats affiche |
---
## 12. Domain 10 — Back-office authentication
## 12. Domaine 10 — Authentification back-office
### 12.1 AUTHENTICATE_USER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | An actor submits the login form |
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
| **Synchronisation** | None |
| **Condition** | Account not in a throttling window (`lockout_until`). Email exists in database. Password matches argon2id hash. User `is_active=1`. |
| **Evenement declencheur** | Un acteur soumet le formulaire de connexion |
| **Acteur** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
| **Synchronisation** | Aucune |
| **Condition** | Le compte n'est pas dans une fenetre de throttling (`lockout_until`). L'email existe en base. Le mot de passe correspond au hash argon2id. L'utilisateur `is_active=1`. |
| **Operation** | AUTHENTICATE_USER |
| **Description** | Credential verification. If valid: session ID regeneration (protection against session fixation), storage of `user_id` and `role_id` in session, UPDATE `last_login_at`, reset of the login failure counter. On failure: increment `failed_login_attempts` and apply a degressive backoff (`lockout_until`), enumeration-safe generic error. Idle timeout: 4h. Absolute timeout: 10h. Redirect to `role.default_route`. See `mlt.md` 12.1. |
| **MCD entities** | R: `user` (verification), `role` (load permissions, default_route), `role_permission`, `login_throttle` (the per-IP throttle gate) — W: `user` (UPDATE last_login_at, `failed_login_attempts`, `lockout_until`), `login_throttle` (upsert `failed_attempts`/`lockout_until` on failure, clear on success), `audit_log` (INSERT login success/failure) |
| **Result** | Session opened, redirect to role-specific default view; or throttled failure logged |
| **Description** | Verification des identifiants. Si valide : regeneration de l'ID de session (protection contre la fixation de session), stockage de `user_id` et `role_id` en session, UPDATE `last_login_at`, remise a zero du compteur d'echecs de connexion. En cas d'echec : incrementation de `failed_login_attempts` et application d'un backoff degressif (`lockout_until`), erreur generique resistante a l'enumeration. Idle timeout : 4h. Absolute timeout : 10h. Redirection vers `role.default_route`. Voir `mlt.md` 12.1. |
| **Entites MCD** | R: `user` (verification), `role` (load permissions, default_route), `role_permission`, `login_throttle` (the per-IP throttle gate) — W: `user` (UPDATE last_login_at, `failed_login_attempts`, `lockout_until`), `login_throttle` (upsert `failed_attempts`/`lockout_until` on failure, clear on success), `audit_log` (INSERT login success/failure) |
| **Resultat** | Session ouverte, redirection vers la vue par defaut specifique au role ; ou echec throttle journalise |
---
### 12.2 LOGOUT_USER
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | Actor clicks "Logout" OR session expires |
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN / SYS (expiry) |
| **Evenement declencheur** | L'acteur clique sur « Deconnexion » OU la session expire |
| **Acteur** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN / SYS (expiration) |
| **Synchronisation** | OR |
| **Condition** | A valid session is open |
| **Condition** | Une session valide est ouverte |
| **Operation** | LOGOUT_USER |
| **Description** | PHP session destruction (`session_destroy()`). Session deleted server-side. Session cookie invalidated. |
| **MCD entities** | No database write (session management is in PHP native, outside DB for this project) |
| **Result** | Session destroyed, redirect to login page |
| **Description** | Destruction de la session PHP (`session_destroy()`). Session supprimee cote serveur. Cookie de session invalide. |
| **Entites MCD** | Aucune ecriture en base (la gestion des sessions est en PHP natif, hors base pour ce projet) |
| **Resultat** | Session detruite, redirection vers la page de connexion |
---
### 12.3 RESET_PASSWORD (security-by-design)
| Field | Value |
| Champ | Valeur |
|-------|-------|
| **Triggering event** | A user requests a password reset, then confirms it via the emailed link |
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
| **Synchronisation** | Sequential two-phase: request, then confirm |
| **Condition** | Request: the submitted email is processed enumeration-safely (same neutral response whether or not it exists). Confirm: a valid, non-expired token is presented. |
| **Evenement declencheur** | Un utilisateur demande une reinitialisation de mot de passe, puis la confirme via le lien envoye par email |
| **Acteur** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
| **Synchronisation** | Sequentielle en deux phases : demande, puis confirmation |
| **Condition** | Demande : l'email soumis est traite de maniere resistante a l'enumeration (meme reponse neutre qu'il existe ou non). Confirmation : un token valide et non expire est presente. |
| **Operation** | RESET_PASSWORD |
| **Description** | Request phase generates a random token, stores its hash + expiry, and e-mails the raw token once. Confirm phase validates the token hash + expiry, replaces `password_hash` (argon2id), clears the token, and resets the login failure counter. See `mlt.md` 12.3. |
| **MCD entities** | W: `user` (UPDATE `password_reset_token_hash` + `password_reset_expires_at` on request; UPDATE `password_hash`, clear token, reset `failed_login_attempts`/`lockout_until` on confirm), `audit_log` (INSERT) |
| **Result** | Password reset via a one-time, time-bound token; one `audit_log` row recorded |
| **Description** | La phase de demande genere un token aleatoire, stocke son hash + expiration, et envoie le token brut une seule fois par email. La phase de confirmation valide le hash du token + expiration, remplace `password_hash` (argon2id), efface le token et remet a zero le compteur d'echecs de connexion. Voir `mlt.md` 12.3. |
| **Entites MCD** | W: `user` (UPDATE `password_reset_token_hash` + `password_reset_expires_at` on request; UPDATE `password_hash`, clear token, reset `failed_login_attempts`/`lockout_until` on confirm), `audit_log` (INSERT) |
| **Resultat** | Mot de passe reinitialise via un token a usage unique et a duree limitee ; une ligne `audit_log` enregistree |
---
## 13. State machine — customer_order.status
## 13. Machine a etats — customer_order.status
Summary of transitions covered by MCT operations.
Recapitulatif des transitions couvertes par les operations MCT.
```
[CUSTOMER / COUNTER / DRIVE]
@ -573,20 +573,20 @@ Summary of transitions covered by MCT operations.
[ cancelled ] (terminal)
```
**Note on the `pending_payment -> paid` transition**: in the RNCP context, payment is
replaced by the customer entering their order number (kiosk) or by staff validation
(counter/drive). The transition is atomic within CREATE_ORDER and CREATE_COUNTER_ORDER.
The `pending_payment` status is not observable outside the transaction.
**Note sur la transition `pending_payment -> paid`** : dans le contexte RNCP, le paiement est
remplace par la saisie du numero de commande par le client (kiosk) ou par la validation du personnel
(comptoir/drive). La transition est atomique au sein de CREATE_ORDER et CREATE_COUNTER_ORDER.
Le statut `pending_payment` n'est pas observable en dehors de la transaction.
**Dropped from v0.1**: `preparing` and `ready` states; `MARK_IN_PREPARATION` and `MARK_READY`
operations. Kitchen staff have a read-only view of `paid` orders (LIST_ORDERS_DISPLAY). The
single delivery action (DELIVER_ORDER) collapses the v0.1 three-step sequence into one gesture.
**Supprime de v0.1** : etats `preparing` et `ready` ; operations `MARK_IN_PREPARATION` et `MARK_READY`.
Le personnel cuisine a une vue en lecture seule des commandes `paid` (LIST_ORDERS_DISPLAY). L'unique
action de livraison (DELIVER_ORDER) condense la sequence en trois etapes de v0.1 en un seul geste.
---
## 14. Operations summary table
## 14. Tableau recapitulatif des operations
| # | Operation | Domain | Actor | W Entities | R Entities |
| # | Operation | Domaine | Acteur | Entites W | Entites R |
|---|-----------|--------|-------|------------|------------|
| 1 | LOAD_CATALOGUE | Order kiosk | CUSTOMER | — | category, product, menu, menu_slot, menu_slot_option, ingredient, allergen, ingredient_allergen |
| 2 | COMPOSE_CART | Order kiosk | CUSTOMER | — (volatile) | product, menu, menu_slot, menu_slot_option, ingredient, product_ingredient |
@ -617,22 +617,22 @@ single delivery action (DELIVER_ORDER) collapses the v0.1 three-step sequence in
| 27 | ERASE_USER_PII | RBAC | ADMIN | user, audit_log | user |
| 28 | RESET_PASSWORD | Auth | ALL BACK | user, audit_log | user |
**Total: 28 operations** (26 prod-like + `ERASE_USER_PII` and `RESET_PASSWORD` from the
security-by-design layer).
**Total : 28 operations** (26 prod-like + `ERASE_USER_PII` et `RESET_PASSWORD` de la
couche security-by-design).
**Audit log writes (security-by-design)**: the sensitive operations 7.1 (cancel), 8.2/8.3
(product update/delete), 8.6 (menu delete), 10.1-10.5 (user/RBAC/erasure) and 12.1 (login)
also write an `audit_log` row (W entity not repeated per row above to keep the table legible).
Stock operations 9.1/9.2 record their attribution via `stock_movement.user_id`. PIN-gated set
per `mlt.md` RG-T13.
**Ecritures du journal d'audit (security-by-design)** : les operations sensibles 7.1 (annulation), 8.2/8.3
(modification/suppression de produit), 8.6 (suppression de menu), 10.1-10.5 (utilisateur/RBAC/effacement) et 12.1 (connexion)
ecrivent egalement une ligne `audit_log` (entite W non repetee par ligne ci-dessus pour garder le tableau lisible).
Les operations de stock 9.1/9.2 enregistrent leur attribution via `stock_movement.user_id`. Ensemble protege par PIN
selon `mlt.md` RG-T13.
---
## 15. MCT -> MCD cross-validation (mantra #34)
## 15. Verification croisee MCT -> MCD (mantra #34)
Verification that each MCD entity participates in at least one MCT operation.
Verification que chaque entite MCD participe a au moins une operation MCT.
| MCD entity | Operations that read | Operations that write | Coverage |
| Entite MCD | Operations en lecture | Operations en ecriture | Couverture |
|------------|---------------------|----------------------|----------|
| `category` | 1, 9, 12, 15 | 15 | OK |
| `product` | 1, 2, 3, 5, 9, 11, 12 | 9, 10, 11 | OK |
@ -653,20 +653,20 @@ Verification that each MCD entity participates in at least one MCT operation.
| `permission` | 23 | — (static seed) | OK (*) |
| `role_permission` | 25 | 23 | OK |
| `stock_movement` | 19 | 3, 5, 8, 17, 18 | OK |
| `audit_log` | (admin audit view) | 8, 10, 11, 14, 20, 21, 22, 23, 25, 27, 28 | OK |
| `audit_log` | (vue d'audit admin) | 8, 10, 11, 14, 20, 21, 22, 23, 25, 27, 28 | OK |
| `login_throttle` | 25 | 25 | OK |
(*) `allergen` and `permission` are read-only at the MCT level: their values are declared
in seed migrations and are not modifiable via the UI. `allergen` is managed indirectly
via `ingredient_allergen` in MANAGE_INGREDIENT.
(*) `allergen` et `permission` sont en lecture seule au niveau MCT : leurs valeurs sont declarees
dans les migrations de seed et ne sont pas modifiables via l'UI. `allergen` est gere indirectement
via `ingredient_allergen` dans MANAGE_INGREDIENT.
(**) `audit_log` (entity 20, security-by-design) is write-mostly: it is appended by the
sensitive operations above and read through an admin audit view (a dedicated read operation
can be formalised when the audit UI is specified at P3).
(**) `audit_log` (entite 20, security-by-design) est principalement en ecriture : il est ajoute par les
operations sensibles ci-dessus et lu via une vue d'audit admin (une operation de lecture dediee
peut etre formalisee lorsque l'UI d'audit sera specifiee en P3).
(***) `login_throttle` (entity 21, security-by-design) is the per-source-IP brute-force
throttle gate: it is read AND written (upserted) by `AUTHENTICATE_USER` (25). Its daily purge
of stale rows is a cron, documented in `mlt.md`, outside MCT operation scope.
(***) `login_throttle` (entite 21, security-by-design) est le verrou de throttling anti-force-brute par IP source :
il est lu ET ecrit (upserte) par `AUTHENTICATE_USER` (25). Sa purge quotidienne
des lignes obsoletes est un cron, documente dans `mlt.md`, hors du perimetre des operations MCT.
**Conclusion**: 21/21 entities covered (19 prod-like + `audit_log` + `login_throttle`). MCT <-> MCD consistency
validated.
**Conclusion** : 21/21 entites couvertes (19 prod-like + `audit_log` + `login_throttle`). Coherence MCT <-> MCD
validee.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff