Compare commits
8 commits
68db2eef0d
...
392ba9a040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
392ba9a040 | ||
|
|
6057ef990f | ||
|
|
36332b4284 | ||
|
|
6c1cede3f0 | ||
|
|
6ceebf7fb1 | ||
|
|
de355da54c | ||
|
|
b8cb3ef68d | ||
|
|
64f5a279da |
21 changed files with 4640 additions and 413 deletions
121
docs/journal/2026-06-04--conception-prodlike-revision.md
Normal file
121
docs/journal/2026-06-04--conception-prodlike-revision.md
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# Conception P1 — revue d'alignement + revision prod-like du modele de donnees
|
||||||
|
|
||||||
|
**Date** : 2026-06-04
|
||||||
|
**Branche** : `feat/p1-conception`
|
||||||
|
**PR** : a venir (apres reecriture des docs Merise)
|
||||||
|
**Duree estimee** : session de decision (point par point)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui a ete fait
|
||||||
|
|
||||||
|
1. **Revue d'alignement complete** de tous les `.md` du projet (PROJECT_CONTEXT, dictionary, mcd, mct, mlt, mld, UML, les 13 notes de `docs/notes/`, le journal) pour verifier que la conception P1 ne derive pas du cadrage. Synthese dans `docs/notes/revue-alignement-p1.md` (non versionne).
|
||||||
|
2. **Session de decision point par point** sur le modele de donnees. Les decisions ci-dessous remplacent ou precisent plusieurs choix du dictionnaire / MCD / MLD v0.1.
|
||||||
|
3. **Principe directeur acte** : le produit vise est **prod-like, pas MVP**. Tout ce qui est decide est implemente dans le livrable final.
|
||||||
|
|
||||||
|
Aucune reecriture des docs Merise n'a encore ete faite : cette session fige les decisions, la propagation dans les 5 docs Merise + PROJECT_CONTEXT se fera en une passe une fois les points D4-D8 tranches.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pourquoi — decisions et alternatives
|
||||||
|
|
||||||
|
### Decision 1 — Suppression de `commande_event`, traçabilite par timestamps de phase
|
||||||
|
|
||||||
|
- **Decision** : abandon de la table d'audit append-only `commande_event`. La traçabilite passe par `commande.status` (etat courant) + une colonne `DATETIME` par phase (`paid_at`, `preparing_at`, `ready_at`, `delivered_at`), plus `created_at`.
|
||||||
|
- **Alternatives** : (A) garder `commande_event` ; (B) colonnes `created_by_user_id` denormalisees ; (C) retrait total de la traçabilite.
|
||||||
|
- **Raison** : en restaurant, le compte back-office est **partage par poste** (cuisine, accueil), pas individuel. L'attribution par personne n'a donc pas de valeur. Le besoin reel est : compter par canal et mesurer les **durees entre phases**. Les timestamps par phase couvrent durees + heures de la journee (stats `service_day`, KPI), sans la complexite d'un journal d'evenements. Mantra #37 (Ockham).
|
||||||
|
|
||||||
|
### Decision 2 — Convention de nommage anglaise, par couche
|
||||||
|
|
||||||
|
- **Decision** : tout en anglais. BDD en `snake_case`, classes PHP en `PascalCase`, methodes/proprietes PHP et JS en `camelCase`, JSON/API en `camelCase`.
|
||||||
|
- **Alternatives** : conserver le francais (dictionnaire v0.1) ; `camelCase` jusque dans les noms de tables SQL.
|
||||||
|
- **Raison** : le `snake_case` reste la convention courante en SQL et evite les pieges de sensibilite a la casse des noms de tables sous Linux/Docker (deja rencontres en Session 4 sur les noms d'images). Le `camelCase` reste la ou il est standard (code PHP/JS). Resout le point D3 (valeur `source` : `comptoir` -> `counter`).
|
||||||
|
|
||||||
|
### Decision 3 — Machine a etats avec phase de paiement
|
||||||
|
|
||||||
|
- **Decision** : conservation de la machine deux phases `pending_payment -> paid -> preparing -> ready -> delivered` (+ `cancelled`). Transition `pending_payment -> paid` atomique a la creation dans le cadre RNCP (saisie du numero = substitut de paiement).
|
||||||
|
- **Raison** : une borne fast-food reelle encaisse a la borne ; modeliser la phase paiement reflete le metier et laisse la porte ouverte a un vrai paiement sans migration destructive. Cout d'une valeur d'ENUM.
|
||||||
|
|
||||||
|
### Decision 4 — TVA portee par le produit, pas par le mode de consommation (apres fact-check)
|
||||||
|
|
||||||
|
- **Decision** : la TVA devient un attribut du produit (`vat_rate`), 10 % par defaut, 5,5 % sur les items en contenant conservable (eau, jus en bouteille). La TVA de la commande se calcule ligne par ligne, taux snapshote sur `order_item`. `mode_consommation` est renomme `service_mode` et conserve **uniquement** pour les stats/KPI (sur place / a emporter / drive), sans role fiscal.
|
||||||
|
- **Alternative ecartee** : la regle initiale du dictionnaire « 10 % sur place / 5,5 % a emporter ».
|
||||||
|
- **Raison** : fact-check de la regle initiale contre la doctrine fiscale officielle. Resultat : le taux depend de la **nature consommation immediate vs differee**, pas du mode sur place / a emporter. Voir bloc FACT-CHECK ci-dessous.
|
||||||
|
|
||||||
|
```
|
||||||
|
FACT-CHECK
|
||||||
|
Claim audite : "TVA 10% sur place / 5,5% a emporter" (dictionnaire note 9, mlt RG-2)
|
||||||
|
Domaine : compliance (fiscal)
|
||||||
|
Verdict : le claim initial est INEXACT
|
||||||
|
Source : BOFiP BOI-ANNX-000495 + BOI-TVA-LIQ-30-10-10 (doctrine officielle impots.gouv.fr)
|
||||||
|
Regle reelle : 10% pour la consommation immediate (sur place OU a emporter) ;
|
||||||
|
5,5% pour les produits en contenant conservable (bouteille, canette) / consommation differee
|
||||||
|
Confiance : 95% (L1, texte officiel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision 5 (D1) — Personnalisation reelle des menus
|
||||||
|
|
||||||
|
- **Decision** : un menu est bati autour d'un **burger fixe** ; le client choisit accompagnement, boisson, sauce. Format **Normal / Maxi** au niveau du menu, qui fait basculer accompagnement + boisson en grande taille et change le prix (deux prix par menu : `price_normal_cents`, `price_maxi_cents`). A la carte, la taille existe la ou la donnee la porte (frites, potatoes). Modele relationnel : table `menu_slot` (emplacements a choix) + `order_item_selection` (choix du client).
|
||||||
|
- **Alternative ecartee** : stocker les choix en bloc JSON ; menu combo fige.
|
||||||
|
- **Raison** : une borne sans choix reel ne reflete pas le metier. Le relationnel est interrogeable (stats KPI : boisson la plus prise, % grandes tailles) et plus defendable au jury Bloc 2 que du JSON opaque.
|
||||||
|
- **Calibrage prix Maxi** : le supplement Maxi est derive de la donnee (Grande Frite 3,50 − Moyenne Frite 2,75 = 0,75) plus un upsize boisson comparable, soit ~1,50 €. Cross-check marche reel (McDonald's France, ecart Best Of -> Maxi Best Of ~1,50-2 € en 2026) : coherent. Wakdo etant un pastiche fictif, on derive de la donnee plutot que copier les prix reels.
|
||||||
|
|
||||||
|
### Decision 6 (D2) — Configurateur d'ingredients complet
|
||||||
|
|
||||||
|
- **Decision** : personnalisation au niveau ingredient (retirer = gratuit, ajouter = supplement) sur **tous les sandwichs composes** (burgers, wraps, cheeseburger), aussi bien a la carte que dans un menu. Tables : `ingredient`, `product_ingredient` (composition par defaut + retirable + ajoutable + supplement), `order_item_modifier` (modifications a la commande).
|
||||||
|
- **Alternatives** : note texte libre ; jeu d'options legeres ; report post-MVP.
|
||||||
|
- **Raison** : choix prod-like assume par l'auteur. Les compositions reelles seront saisies en seed (recuperees publiquement, coherent avec un catalogue deja calque sur des produits connus).
|
||||||
|
|
||||||
|
### Decision 7 — Modal allergenes derivee des ingredients
|
||||||
|
|
||||||
|
- **Decision** : table `allergen` + `ingredient_allergen`. Les allergenes d'un produit sont **calcules** par jointure sur sa composition, sans ressaisie manuelle. Affichage en modal sur la borne pour chaque produit.
|
||||||
|
- **Raison** : reutilise la donnee ingredient (Decision 6) sans duplication ; coherence garantie. Aligne avec le reglement INCO (UE) 1169/2011 (declaration des 14 allergenes ; liste officielle a confirmer au seed). Nourrit l'accessibilite du Bloc 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comment — points techniques cles
|
||||||
|
|
||||||
|
- **Taille / format unifies** : une notion `normal` / `maxi`. A la carte, la taille existe via des produits distincts (la donnee ecole modelise « Petite/Moyenne/Grande Frite » comme 3 produits). En menu, le format est un attribut de la ligne menu qui cascade sur les composants (pas de prix individuel, compris dans le prix combo).
|
||||||
|
- **Snapshots** : prix unitaire ET taux TVA sont snapshotes sur `order_item` au moment de la commande (integrite historique, meme logique que le snapshot de libelle deja prevu).
|
||||||
|
- **Personnalisation du burger dans un menu** : les modifications (`order_item_modifier`) doivent pouvoir s'attacher au burger qu'il soit pris seul ou comme burger fixe d'un menu. Materialisation a preciser au DDL.
|
||||||
|
- **Couleurs KDS back-office** : calculees a l'affichage (`maintenant − paid_at` vs seuil SLA global ~10 min en config), aucune donnee supplementaire a stocker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criteres RNCP couverts
|
||||||
|
|
||||||
|
- **Bloc 2 - Cr 3.a / 3.b** : analyse et modelisation des donnees (dictionnaire, MCD, MLD), passage relationnel, contraintes referentielles, polymorphisme, snapshots.
|
||||||
|
- **Bloc 2 - Cr 3.d** : la TVA correcte et le calcul ligne par ligne demontrent la rigueur sur la donnee.
|
||||||
|
- **Bloc 1 - Cr 1.c** : la modal allergenes renforce l'accessibilite / l'information utilisateur.
|
||||||
|
- **Compliance / fact-check** : la regle TVA est sourcee L1 (BOFiP), conforme au protocole `.claude/rules/fact-check.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions anticipees du jury
|
||||||
|
|
||||||
|
- **Q** : "Pourquoi avoir abandonne le journal d'evenements de commande ?"
|
||||||
|
**R** : Le compte back-office est partage par poste, donc l'attribution individuelle d'une transition n'a pas de valeur metier. Le besoin reel (durees entre phases, heures) est couvert par des timestamps par phase sur la commande, sans la complexite d'un event store.
|
||||||
|
|
||||||
|
- **Q** : "Vous appliquez 5,5 % a l'emporter ?"
|
||||||
|
**R** : Non. Apres verification du BOFiP, le taux depend de la consommation immediate ou differee, pas du mode sur place / a emporter. En fast-food, ce qui est chaud / en gobelet est a 10 % dans les deux cas ; le 5,5 % concerne les contenants conservables (bouteille, canette). La TVA est donc portee par le produit.
|
||||||
|
|
||||||
|
- **Q** : "Comment gerez-vous les allergenes sans les ressaisir pour chaque produit ?"
|
||||||
|
**R** : Ils sont modelises au niveau ingredient. Les allergenes d'un produit sont calcules par jointure sur sa composition. Modifier un ingredient met a jour tous les produits concernes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Points d'amelioration conscients
|
||||||
|
|
||||||
|
- **Scope volontairement etendu** : le modele passe de ~11 a ~16 entites (configurateur d'ingredients + allergenes + selections de menu). Choix prod-like assume. Consequence : `PROJECT_CONTEXT` §7 (scope, mot « MVP » a retirer, items a deplacer en IN scope) et §11 (planning / budget heures) sont a rechiffrer pour rester honnetes.
|
||||||
|
- **Docs Merise a reecrire** : dictionary, mcd, mct, mlt, mld doivent etre repris en une passe (anglais, 16 entites, prod-like) une fois les decisions restantes tranchees. Reecriture differee volontairement pour ne pas toucher ces docs deux fois.
|
||||||
|
- **Decisions encore ouvertes** (a trancher avant la reecriture) : D4 (liste des roles unifiee), D5 (vocabulaire des permissions), D6 (correction de la formule `service_day` — coupure a 10h, pas 4h30), D7 (subnet Docker : doc vs realite), D8 (prefixe du numero de commande).
|
||||||
|
- **Diagrammes** : MCD et MLD a regenerer pour refleter le modele a 16 entites.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Liens vers artefacts
|
||||||
|
|
||||||
|
- Revue d'alignement : `docs/notes/revue-alignement-p1.md` (non versionne)
|
||||||
|
- Docs impactes a venir : `docs/merise/{dictionary,mcd,mct,mlt,mld}.md`, `docs/PROJECT_CONTEXT.md`
|
||||||
|
- Sources fact-check TVA : BOFiP BOI-ANNX-000495, BOI-TVA-LIQ-30-10-10 (impots.gouv.fr)
|
||||||
|
- Reference prix Maxi : mcdonalds.fr (menus Best Of / Maxi Best Of), cross-check de magnitude uniquement
|
||||||
|
|
@ -30,6 +30,7 @@ Les fichiers sont ordonnes chronologiquement par leur nom.
|
||||||
| 2026-04-23 | [cadrage-projet](2026-04-23--cadrage-projet.md) | Analyse brief RNCP, decisions d'architecture, bootstrap Git | `main` (commit initial) |
|
| 2026-04-23 | [cadrage-projet](2026-04-23--cadrage-projet.md) | Analyse brief RNCP, decisions d'architecture, bootstrap Git | `main` (commit initial) |
|
||||||
| 2026-04-24 | [infra-docker](2026-04-24--infra-docker.md) | Stack Docker complete (compose + 4 services), referentiel RNCP integre, cross-check mappings Cr 4.f | `feat/infra-docker` |
|
| 2026-04-24 | [infra-docker](2026-04-24--infra-docker.md) | Stack Docker complete (compose + 4 services), referentiel RNCP integre, cross-check mappings Cr 4.f | `feat/infra-docker` |
|
||||||
| 2026-04-30 | [smoke-test-infra](2026-04-30--smoke-test-infra.md) | Smoke test bout-en-bout sur serveur reel : fusion .env, switch FQDN sur stark.a3n.fr, subnet explicite RFC 1918, fix init cron + healthz | `feat/infra-docker` |
|
| 2026-04-30 | [smoke-test-infra](2026-04-30--smoke-test-infra.md) | Smoke test bout-en-bout sur serveur reel : fusion .env, switch FQDN sur stark.a3n.fr, subnet explicite RFC 1918, fix init cron + healthz | `feat/infra-docker` |
|
||||||
|
| 2026-06-04 | [conception-prodlike-revision](2026-06-04--conception-prodlike-revision.md) | Revue d'alignement P1 + decisions prod-like du modele de donnees (drop commande_event, nommage EN, TVA par produit apres fact-check BOFiP, perso menus/ingredients, allergenes, ~16 entites) | `feat/p1-conception` |
|
||||||
|
|
||||||
*Mis a jour a chaque nouvelle entree.*
|
*Mis a jour a chaque nouvelle entree.*
|
||||||
|
|
||||||
|
|
|
||||||
67
docs/merise/_diagrams/mcd-catalogue.drawio
Normal file
67
docs/merise/_diagrams/mcd-catalogue.drawio
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||||
|
<diagram name="MCD - Catalogue" id="mcd-catalogue">
|
||||||
|
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<mxCell id="categorie" value="<b>CATEGORIE</b><hr>id : INT (PK)<br>libelle : VARCHAR (UNIQUE)<br>slug : VARCHAR (UNIQUE)<br>image_path : VARCHAR<br>ordre : SMALLINT<br>est_actif : BOOLEAN" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="40" width="280" height="160" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="produit" value="<b>PRODUIT</b><hr>id : INT (PK)<br>categorie_id : INT (FK)<br>libelle : VARCHAR<br>description : TEXT<br>prix_ttc_cents : INT<br>image_path : VARCHAR<br>est_disponible : BOOLEAN<br>ordre : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="320" width="280" height="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="menu" value="<b>MENU</b><hr>id : INT (PK)<br>categorie_id : INT (FK)<br>libelle : VARCHAR<br>description : TEXT<br>prix_ttc_cents : INT<br>image_path : VARCHAR<br>est_disponible : BOOLEAN<br>ordre : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="820" y="320" width="280" height="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="menu_produit" value="<b>MENU_PRODUIT</b> <i>(associative)</i><hr>menu_id : INT (PK, FK)<br>produit_id : INT (PK, FK)<br>role : ENUM<br>position : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="640" width="280" height="140" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_cat_prod" value="regroupe" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cat_prod_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_prod">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cat_prod_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_prod">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_cat_menu" value="regroupe" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="menu">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cat_menu_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_menu">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cat_menu_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cat_menu">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_prod_mp" value="fait_partie_de" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="produit" target="menu_produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_prod_mp_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_prod_mp">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_prod_mp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_prod_mp">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_menu_mp" value="compose" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="menu" target="menu_produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_menu_mp_a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_menu_mp">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_menu_mp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_menu_mp">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
4
docs/merise/_diagrams/mcd-catalogue.svg
Normal file
4
docs/merise/_diagrams/mcd-catalogue.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 28 KiB |
93
docs/merise/_diagrams/mcd-commande.drawio
Normal file
93
docs/merise/_diagrams/mcd-commande.drawio
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||||
|
<diagram name="MCD - Commande" id="mcd-commande">
|
||||||
|
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<mxCell id="commande" value="<b>COMMANDE</b><hr>id : INT (PK)<br>numero : VARCHAR (UNIQUE)<br>source : ENUM (kiosk|comptoir|drive)<br>mode_consommation : ENUM (sur_place|a_emporter|drive)<br>statut : ENUM<br>total_ht_cents : INT<br>total_tva_cents : INT<br>total_ttc_cents : INT<br>tva_taux_pourmille : SMALLINT<br>paye_a : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="40" width="320" height="240" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="user_stub" value="<b>USER</b><hr>id : INT (PK)<br><i>(detail dans RBAC)</i>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="880" y="40" width="240" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="commande_event" value="<b>COMMANDE_EVENT</b><hr>id : INT (PK)<br>commande_id : INT (FK)<br>event_type : ENUM<br>from_statut : ENUM (NULL)<br>to_statut : ENUM<br>user_id : INT (FK, NULL)<br>payload : JSON (NULL)<br>created_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="840" y="360" width="280" height="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="ligne_commande" value="<b>LIGNE_COMMANDE</b><hr>id : INT (PK)<br>commande_id : INT (FK)<br>type_item : ENUM (produit|menu)<br>produit_id : INT (FK, NULL)<br>menu_id : INT (FK, NULL)<br>libelle_snapshot : VARCHAR<br>prix_unitaire_ttc_cents_snapshot : INT<br>quantite : SMALLINT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="360" width="280" height="220" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="produit" value="<b>PRODUIT</b><hr>id : INT (PK)<br><i>(detail dans Catalogue)</i>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="80" y="700" width="240" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="menu" value="<b>MENU</b><hr>id : INT (PK)<br><i>(detail dans Catalogue)</i>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="840" y="700" width="240" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_cmd_lc" value="contient" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="ligne_commande">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cmd_lc_a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_lc">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cmd_lc_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_lc">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_lc_prod" value="refere_si_type_produit" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_lc_prod_a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_prod">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_lc_prod_b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_prod">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_lc_menu" value="refere_si_type_menu" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="menu">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_lc_menu_a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_menu">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_lc_menu_b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_lc_menu">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="note_poly" value="<b>Polymorphisme</b><br>Exactement UNE des deux references est non-nulle.<br>Discriminateur : type_item &isin; {produit, menu}.<br>Contrainte CHECK SQL au MLD." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="80" y="360" width="280" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_cmd_evt" value="journalise" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="commande_event">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cmd_evt_a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_evt">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_cmd_evt_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_cmd_evt">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_user_evt" value="declenche" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="user_stub" target="commande_event">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_user_evt_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_evt">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_user_evt_b" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_evt">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="note_audit" value="<b>Journal d'audit (event sourcing)</b><br>Append-only : aucun UPDATE / DELETE applicatif.<br>user_id NULL si auto-validation kiosk.<br>ON DELETE CASCADE cote commande_id.<br>ON DELETE SET NULL cote user_id." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="840" y="580" width="280" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
4
docs/merise/_diagrams/mcd-commande.svg
Normal file
4
docs/merise/_diagrams/mcd-commande.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 704 KiB |
182
docs/merise/_diagrams/mcd-global.drawio
Normal file
182
docs/merise/_diagrams/mcd-global.drawio
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||||
|
<diagram name="MCD - Global" id="mcd-global">
|
||||||
|
<mxGraphModel dx="1800" dy="1100" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<mxCell id="categorie" value="<b>CATEGORIE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="40" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="produit" value="<b>PRODUIT</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="240" y="220" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="menu_produit" value="<b>MENU_PRODUIT</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;align=center;verticalAlign=middle;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="220" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="menu" value="<b>MENU</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="960" y="220" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="ligne_commande" value="<b>LIGNE_COMMANDE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="400" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="commande" value="<b>COMMANDE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="540" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="commande_event" value="<b>COMMANDE_EVENT</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="960" y="540" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="user" value="<b>USER</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="120" y="780" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="role" value="<b>ROLE</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="780" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="role_permission" value="<b>ROLE_PERMISSION</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=13;align=center;verticalAlign=middle;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="760" y="780" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="permission" value="<b>PERMISSION</b>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=13;align=center;verticalAlign=middle" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1080" y="780" width="200" height="50" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e1" value="regroupe" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e1a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e1">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e1b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e1">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e2" value="regroupe" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="categorie" target="menu">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e2a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e2">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e2b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e2">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e3" value="fait_partie_de" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="produit" target="menu_produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e3a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e3">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e3b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e3">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e4" value="compose" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="menu" target="menu_produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e4a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e4">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e4b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e4">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e5" value="contient" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="ligne_commande">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e5a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e5">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e5b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e5">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e6" value="refere_si_type_produit" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="produit">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="120" y="425" />
|
||||||
|
<mxPoint x="120" y="245" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e6a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e6">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e6b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e6">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e7" value="refere_si_type_menu" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="ligne_commande" target="menu">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1280" y="425" />
|
||||||
|
<mxPoint x="1280" y="245" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e7a" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e7">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e7b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e7">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e8" value="a_pour_role" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="user" target="role">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e8a" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e8">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e8b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e8">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e9" value="possede" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="role" target="role_permission">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e9a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e9">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e9b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e9">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e10" value="assignee_a" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="permission" target="role_permission">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e10a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e10">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e10b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e10">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e11" value="journalise" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="commande" target="commande_event">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e11a" value="(1,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e11">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e11b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e11">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e12" value="declenche" style="endArrow=none;html=1;fontSize=11;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0;dashed=1" edge="1" parent="1" source="user" target="commande_event">
|
||||||
|
<mxGeometry relative="1" as="geometry">
|
||||||
|
<Array as="points">
|
||||||
|
<mxPoint x="1280" y="805" />
|
||||||
|
<mxPoint x="1280" y="565" />
|
||||||
|
</Array>
|
||||||
|
</mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e12a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e12">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e12b" value="(0,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e12">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
4
docs/merise/_diagrams/mcd-global.svg
Normal file
4
docs/merise/_diagrams/mcd-global.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 363 KiB |
57
docs/merise/_diagrams/mcd-rbac.drawio
Normal file
57
docs/merise/_diagrams/mcd-rbac.drawio
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||||
|
<diagram name="MCD - RBAC" id="mcd-rbac">
|
||||||
|
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<mxCell id="user" value="<b>USER</b><hr>id : INT (PK)<br>email : VARCHAR (UNIQUE, RFC 5321)<br>password_hash : VARCHAR (argon2id)<br>nom : VARCHAR<br>prenom : VARCHAR<br>role_id : INT (FK)<br>est_actif : BOOLEAN<br>last_login_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="60" y="80" width="280" height="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="role" value="<b>ROLE</b><hr>id : INT (PK)<br>code : VARCHAR (UNIQUE)<br>libelle : VARCHAR<br>description : TEXT<br>est_actif : BOOLEAN" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="80" width="280" height="160" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="permission" value="<b>PERMISSION</b><hr>id : INT (PK)<br>code : VARCHAR (UNIQUE, resource.action)<br>libelle : VARCHAR<br>description : TEXT" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="820" y="80" width="280" height="160" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="role_permission" value="<b>ROLE_PERMISSION</b> <i>(associative)</i><hr>role_id : INT (PK, FK)<br>permission_id : INT (PK, FK)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=12;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="630" y="440" width="300" height="120" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_user_role" value="a_pour_role" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="user" target="role">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_user_role_a" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_role">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_user_role_b" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_user_role">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_role_rp" value="possede" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="role" target="role_permission">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_role_rp_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_role_rp">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_role_rp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_role_rp">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="e_perm_rp" value="assignee_a" style="endArrow=none;html=1;fontSize=12;fontStyle=2;edgeStyle=orthogonalEdgeStyle;rounded=0" edge="1" parent="1" source="permission" target="role_permission">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_perm_rp_a" value="(0,N)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_perm_rp">
|
||||||
|
<mxGeometry x="-0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="e_perm_rp_b" value="(1,1)" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=11;fontStyle=1;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="e_perm_rp">
|
||||||
|
<mxGeometry x="0.85" relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
4
docs/merise/_diagrams/mcd-rbac.svg
Normal file
4
docs/merise/_diagrams/mcd-rbac.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 337 KiB |
59
docs/merise/_diagrams/mld-catalogue.drawio
Normal file
59
docs/merise/_diagrams/mld-catalogue.drawio
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||||
|
<diagram name="MLD - Catalogue" id="mld-catalogue">
|
||||||
|
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<mxCell id="t_categorie" value="<b>categorie</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>UK libelle : VARCHAR(80)<br>UK slug : VARCHAR(60)<br>image_path : VARCHAR(255) NULL<br>ordre : SMALLINT UNSIGNED DEFAULT 0<br>est_actif : TINYINT(1) DEFAULT 1<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="40" width="300" height="180" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_produit" value="<b>produit</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK categorie_id : INT UNSIGNED<br>libelle : VARCHAR(120)<br>description : TEXT NULL<br>prix_ttc_cents : INT UNSIGNED (CHECK > 0)<br>image_path : VARCHAR(255) NULL<br>est_disponible : TINYINT(1) DEFAULT 1<br>ordre : SMALLINT UNSIGNED DEFAULT 0<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="280" width="320" height="220" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_menu" value="<b>menu</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK categorie_id : INT UNSIGNED<br>libelle : VARCHAR(120)<br>description : TEXT NULL<br>prix_ttc_cents : INT UNSIGNED (CHECK > 0)<br>image_path : VARCHAR(255) NULL<br>est_disponible : TINYINT(1) DEFAULT 1<br>ordre : SMALLINT UNSIGNED DEFAULT 0<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="820" y="280" width="320" height="220" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_menu_produit" value="<b>menu_produit</b> (jointure)<hr><u>PK FK menu_id : INT UNSIGNED</u><br><u>PK FK produit_id : INT UNSIGNED</u><br>role : ENUM(burger,accompagnement,boisson,sauce,dessert)<br>position : SMALLINT UNSIGNED DEFAULT 0" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="400" y="560" width="380" height="130" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_prod_cat" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_produit" target="t_categorie">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_prod_cat_lbl" value="FK ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_prod_cat">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_menu_cat" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_menu" target="t_categorie">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_menu_cat_lbl" value="FK ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_menu_cat">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_mp_menu" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_menu_produit" target="t_menu">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_mp_menu_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_mp_menu">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_mp_prod" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_menu_produit" target="t_produit">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_mp_prod_lbl" value="FK ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_mp_prod">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="legende" value="<b>Legende</b><br><u>PK</u> : cle primaire<br>FK : cle etrangere (fleche -> table referencee)<br>UK : contrainte unique<br>Bleu = table principale<br>Jaune pointille = table de jointure" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="40" width="280" height="130" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
78
docs/merise/_diagrams/mld-commande.drawio
Normal file
78
docs/merise/_diagrams/mld-commande.drawio
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||||
|
<diagram name="MLD - Commande" id="mld-commande">
|
||||||
|
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1654" pageHeight="1169" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<mxCell id="t_commande" value="<b>commande</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>UK numero : VARCHAR(20)<br>source : ENUM(kiosk,comptoir,drive)<br>mode_consommation : ENUM(sur_place,a_emporter,drive)<br>statut : ENUM DEFAULT pending_payment<br>total_ht_cents : INT UNSIGNED<br>total_tva_cents : INT UNSIGNED<br>total_ttc_cents : INT UNSIGNED<br>tva_taux_pourmille : SMALLINT UNSIGNED<br>paye_a : DATETIME NULL<br>created_at : DATETIME<br>updated_at : DATETIME<hr>CHECK (source != drive OR mode = drive)<br>CHECK (ttc = ht + tva)" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="500" y="40" width="380" height="290" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_ligne_commande" value="<b>ligne_commande</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK commande_id : INT UNSIGNED<br>type_item : ENUM(produit,menu)<br>FK produit_id : INT UNSIGNED NULL<br>FK menu_id : INT UNSIGNED NULL<br>libelle_snapshot : VARCHAR(120)<br>prix_unitaire_ttc_cents_snapshot : INT UNSIGNED<br>quantite : SMALLINT UNSIGNED DEFAULT 1<br>created_at : DATETIME<hr>CHECK polymorphisme exclusif" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="400" width="380" height="220" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_commande_event" value="<b>commande_event</b> (append-only)<hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>FK commande_id : INT UNSIGNED<br>event_type : ENUM(CREATED,PAID,...)<br>from_statut : ENUM NULL<br>to_statut : ENUM<br>FK user_id : INT UNSIGNED NULL<br>payload : JSON NULL<br>created_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="960" y="400" width="380" height="200" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_produit_stub" value="<b>produit</b> <i>(cf. Catalogue)</i><hr><u>PK id</u><br>..." style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="680" width="200" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_menu_stub" value="<b>menu</b> <i>(cf. Catalogue)</i><hr><u>PK id</u><br>..." style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="280" y="680" width="200" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_user_stub" value="<b>user</b> <i>(cf. RBAC)</i><hr><u>PK id</u><br>..." style="rounded=0;whiteSpace=wrap;html=1;fillColor=#e1d5e7;strokeColor=#9673a6;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="1140" y="680" width="200" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_lc_cmd" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_ligne_commande" target="t_commande">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_lc_cmd_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_lc_cmd">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_lc_prod" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1" edge="1" parent="1" source="t_ligne_commande" target="t_produit_stub">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_lc_prod_lbl" value="FK NULL ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_lc_prod">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_lc_menu" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1" edge="1" parent="1" source="t_ligne_commande" target="t_menu_stub">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_lc_menu_lbl" value="FK NULL ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_lc_menu">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_evt_cmd" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_commande_event" target="t_commande">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_evt_cmd_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_evt_cmd">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_evt_user" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle;dashed=1" edge="1" parent="1" source="t_commande_event" target="t_user_stub">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_evt_user_lbl" value="FK NULL ON DELETE SET NULL" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_evt_user">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="note_audit" value="<b>Journal d'audit (event sourcing)</b><br>Append-only : aucun UPDATE / DELETE applicatif.<br>3 IDX : (commande_id, created_at), (user_id, created_at), (event_type, created_at).<br>Pattern d'ecriture : transaction qui modifie commande.statut insere aussi une ligne d'event." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="960" y="620" width="380" height="80" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="legende" value="<b>Legende</b><br><u>PK</u> : cle primaire<br>FK : cle etrangere (fleche -> table referencee)<br>UK : contrainte unique<br>Bleu = table principale<br>Vert = journal d'audit<br>Violet = stub d'un autre sous-domaine<br>Pointille = FK nullable" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="40" width="280" height="150" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
56
docs/merise/_diagrams/mld-rbac.drawio
Normal file
56
docs/merise/_diagrams/mld-rbac.drawio
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
<mxfile host="app.diagrams.net" agent="claude-code-byan" type="device">
|
||||||
|
<diagram name="MLD - RBAC" id="mld-rbac">
|
||||||
|
<mxGraphModel dx="1400" dy="900" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="826" math="0" shadow="0">
|
||||||
|
<root>
|
||||||
|
<mxCell id="0" />
|
||||||
|
<mxCell id="1" parent="0" />
|
||||||
|
|
||||||
|
<mxCell id="t_user" value="<b>user</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>UK email : VARCHAR(254)<br>password_hash : VARCHAR(255)<br>nom : VARCHAR(60)<br>prenom : VARCHAR(60)<br>FK role_id : INT UNSIGNED<br>est_actif : TINYINT(1) DEFAULT 1<br>last_login_at : DATETIME NULL<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="60" width="320" height="220" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_role" value="<b>role</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>UK code : VARCHAR(40)<br>libelle : VARCHAR(80)<br>description : TEXT NULL<br>est_actif : TINYINT(1) DEFAULT 1<br>created_at : DATETIME<br>updated_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="440" y="60" width="320" height="160" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_permission" value="<b>permission</b><hr><u>PK id : INT UNSIGNED AUTO_INCREMENT</u><br>UK code : VARCHAR(60) format resource.action<br>libelle : VARCHAR(120)<br>description : TEXT NULL<br>created_at : DATETIME" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="840" y="60" width="320" height="140" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="t_role_permission" value="<b>role_permission</b> (jointure)<hr><u>PK FK role_id : INT UNSIGNED</u><br><u>PK FK permission_id : INT UNSIGNED</u>" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=11;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="600" y="380" width="320" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_user_role" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_user" target="t_role">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_user_role_lbl" value="FK ON DELETE RESTRICT" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_user_role">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_rp_role" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_role_permission" target="t_role">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_rp_role_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_rp_role">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="fk_rp_perm" style="endArrow=open;html=1;rounded=0;edgeStyle=orthogonalEdgeStyle" edge="1" parent="1" source="t_role_permission" target="t_permission">
|
||||||
|
<mxGeometry relative="1" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
<mxCell id="fk_rp_perm_lbl" value="FK ON DELETE CASCADE" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=10;fillColor=#ffffff;strokeColor=none" vertex="1" connectable="0" parent="fk_rp_perm">
|
||||||
|
<mxGeometry relative="1" as="geometry"><mxPoint as="offset" /></mxGeometry>
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="note_rbac" value="<b>Modele RBAC</b><br>Roles : dynamiques (CRUD admin UI), table principale role.<br>Permissions : statiques (declarees en migration), pas d'updated_at.<br>Mapping role-permission : matrice editable depuis l'UI admin.<br>Voir docs/notes/rbac-roles-permissions.md." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="380" width="500" height="100" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
<mxCell id="legende" value="<b>Legende</b><br><u>PK</u> : cle primaire (composite si plusieurs lignes soulignees)<br>FK : cle etrangere (fleche -> table referencee)<br>UK : contrainte unique<br>Bleu = table principale<br>Jaune pointille = table de jointure" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#666666;align=left;verticalAlign=top;fontSize=10;spacingLeft=8;spacingTop=4;dashed=1" vertex="1" parent="1">
|
||||||
|
<mxGeometry x="40" y="540" width="500" height="120" as="geometry" />
|
||||||
|
</mxCell>
|
||||||
|
|
||||||
|
</root>
|
||||||
|
</mxGraphModel>
|
||||||
|
</diagram>
|
||||||
|
</mxfile>
|
||||||
File diff suppressed because it is too large
Load diff
533
docs/merise/mcd.md
Normal file
533
docs/merise/mcd.md
Normal file
|
|
@ -0,0 +1,533 @@
|
||||||
|
# Conceptual Data Model (MCD) — Wakdo
|
||||||
|
|
||||||
|
**Merise phase** : P1 - Conception, step 2 (data dictionary first, mantra #33)
|
||||||
|
**Version** : v0.2 — prod-like, 19 entities
|
||||||
|
**Date** : 2026-06-04
|
||||||
|
**Branch** : `feat/p1-conception`
|
||||||
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose of this 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).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Sources**:
|
||||||
|
- `docs/merise/dictionary.md` (v0.2 — 19 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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Merise notation used
|
||||||
|
|
||||||
|
### Cardinalities at the association foot (French Merise style)
|
||||||
|
|
||||||
|
At each end of an association, the cardinality `(min,max)` states how many times an
|
||||||
|
instance of the entity participates in the association.
|
||||||
|
|
||||||
|
```
|
||||||
|
ENTITY_A (min,max) ----[ ASSOCIATION ]---- (min,max) ENTITY_B
|
||||||
|
```
|
||||||
|
|
||||||
|
| Notation | Reading | Example |
|
||||||
|
|---|---|---|
|
||||||
|
| `(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 |
|
||||||
|
|
||||||
|
Reading: "one instance of the source entity participates at least MIN times and at most
|
||||||
|
MAX times in the association".
|
||||||
|
|
||||||
|
### Association naming convention
|
||||||
|
|
||||||
|
Active verb in business terms, e.g.: `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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Decomposition by sub-domain
|
||||||
|
|
||||||
|
The 19-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.
|
||||||
|
|
||||||
|
| Sub-domain | Entities | Count |
|
||||||
|
|---|---|---|
|
||||||
|
| 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 | user, role, role_visible_source, permission, role_permission | 5 |
|
||||||
|
|
||||||
|
**Note on the absence of a global diagram**: a single 19-entity ER diagram would be
|
||||||
|
unreadable and unmaintainable. The sub-domain decomposition below is the intentional
|
||||||
|
structural choice. The `.drawio` source files will be regenerated from this document as the
|
||||||
|
single reference once the MCD is stabilised (regeneration tracked in `docs/notes/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Sub-domain: Catalogue
|
||||||
|
|
||||||
|
### 4.1 Mermaid entity-relationship diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
category {
|
||||||
|
int id PK
|
||||||
|
varchar name
|
||||||
|
varchar slug
|
||||||
|
varchar image_path
|
||||||
|
smallint display_order
|
||||||
|
tinyint is_active
|
||||||
|
}
|
||||||
|
product {
|
||||||
|
int id PK
|
||||||
|
int category_id FK
|
||||||
|
varchar name
|
||||||
|
text description
|
||||||
|
int price_cents
|
||||||
|
smallint vat_rate
|
||||||
|
varchar image_path
|
||||||
|
tinyint is_available
|
||||||
|
smallint display_order
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
int id PK
|
||||||
|
int category_id FK
|
||||||
|
int burger_product_id FK
|
||||||
|
varchar name
|
||||||
|
text description
|
||||||
|
int price_normal_cents
|
||||||
|
int price_maxi_cents
|
||||||
|
varchar image_path
|
||||||
|
tinyint is_available
|
||||||
|
smallint display_order
|
||||||
|
}
|
||||||
|
menu_slot {
|
||||||
|
int id PK
|
||||||
|
int menu_id FK
|
||||||
|
varchar name
|
||||||
|
enum slot_type
|
||||||
|
tinyint is_required
|
||||||
|
smallint display_order
|
||||||
|
}
|
||||||
|
menu_slot_option {
|
||||||
|
int menu_slot_id FK
|
||||||
|
int product_id FK
|
||||||
|
}
|
||||||
|
|
||||||
|
category ||--o{ product : "groups"
|
||||||
|
category ||--o{ menu : "groups"
|
||||||
|
menu ||--|| product : "anchors (burger_product_id)"
|
||||||
|
menu ||--o{ menu_slot : "defines_slot"
|
||||||
|
menu_slot ||--o{ menu_slot_option : "lists"
|
||||||
|
product ||--o{ menu_slot_option : "is_eligible_for"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Association cardinalities
|
||||||
|
|
||||||
|
| # | Association | Side A | Cardinality A | Side B | Cardinality 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. |
|
||||||
|
|
||||||
|
### 4.3 Notes on the Catalogue sub-domain
|
||||||
|
|
||||||
|
**`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.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`.
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Sub-domain: Ingredients & Stock
|
||||||
|
|
||||||
|
### 5.1 Mermaid entity-relationship diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
product {
|
||||||
|
int id PK
|
||||||
|
varchar name
|
||||||
|
}
|
||||||
|
ingredient {
|
||||||
|
int id PK
|
||||||
|
varchar name
|
||||||
|
varchar unit
|
||||||
|
int stock_quantity
|
||||||
|
smallint pack_size
|
||||||
|
varchar pack_label
|
||||||
|
smallint low_stock_threshold
|
||||||
|
tinyint is_active
|
||||||
|
}
|
||||||
|
product_ingredient {
|
||||||
|
int product_id FK
|
||||||
|
int ingredient_id FK
|
||||||
|
smallint quantity
|
||||||
|
tinyint is_removable
|
||||||
|
tinyint is_addable
|
||||||
|
int extra_price_cents
|
||||||
|
}
|
||||||
|
allergen {
|
||||||
|
int id PK
|
||||||
|
varchar code
|
||||||
|
varchar name
|
||||||
|
text description
|
||||||
|
}
|
||||||
|
ingredient_allergen {
|
||||||
|
int ingredient_id FK
|
||||||
|
int allergen_id FK
|
||||||
|
}
|
||||||
|
customer_order {
|
||||||
|
int id PK
|
||||||
|
varchar order_number
|
||||||
|
}
|
||||||
|
user {
|
||||||
|
int id PK
|
||||||
|
varchar email
|
||||||
|
}
|
||||||
|
stock_movement {
|
||||||
|
int id PK
|
||||||
|
int ingredient_id FK
|
||||||
|
enum movement_type
|
||||||
|
int delta
|
||||||
|
int order_id FK
|
||||||
|
int user_id FK
|
||||||
|
varchar note
|
||||||
|
}
|
||||||
|
|
||||||
|
product ||--o{ product_ingredient : "is_composed_of"
|
||||||
|
ingredient ||--o{ product_ingredient : "appears_in"
|
||||||
|
ingredient ||--o{ ingredient_allergen : "contains"
|
||||||
|
allergen ||--o{ ingredient_allergen : "is_present_in"
|
||||||
|
ingredient ||--o{ stock_movement : "decrements"
|
||||||
|
customer_order |o--o{ stock_movement : "triggers"
|
||||||
|
user |o--o{ stock_movement : "logs"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Association cardinalities
|
||||||
|
|
||||||
|
| # | Association | Side A | Cardinality A | Side B | Cardinality 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. |
|
||||||
|
|
||||||
|
### 5.3 Notes on the Ingredients & Stock sub-domain
|
||||||
|
|
||||||
|
**`product_ingredient` as an associative entity**: the N-N association between `product` and `ingredient` carries four attributes (`quantity`, `is_removable`, `is_addable`, `extra_price_cents`). It becomes a join table in the MLD with composite PK `(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.
|
||||||
|
|
||||||
|
**`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`.
|
||||||
|
|
||||||
|
**Low-stock alert**: computed at display time (`stock_quantity <= low_stock_threshold`); no additional stored column.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Sub-domain: Order
|
||||||
|
|
||||||
|
### 6.1 Mermaid entity-relationship diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
customer_order {
|
||||||
|
int id PK
|
||||||
|
varchar order_number
|
||||||
|
enum source
|
||||||
|
enum service_mode
|
||||||
|
enum status
|
||||||
|
int total_ht_cents
|
||||||
|
int total_vat_cents
|
||||||
|
int total_ttc_cents
|
||||||
|
datetime paid_at
|
||||||
|
datetime delivered_at
|
||||||
|
datetime cancelled_at
|
||||||
|
}
|
||||||
|
order_item {
|
||||||
|
int id PK
|
||||||
|
int order_id FK
|
||||||
|
enum item_type
|
||||||
|
int product_id FK
|
||||||
|
int menu_id FK
|
||||||
|
enum format
|
||||||
|
varchar label_snapshot
|
||||||
|
int unit_price_cents_snapshot
|
||||||
|
smallint vat_rate_snapshot
|
||||||
|
smallint quantity
|
||||||
|
}
|
||||||
|
order_item_selection {
|
||||||
|
int id PK
|
||||||
|
int order_item_id FK
|
||||||
|
int menu_slot_id FK
|
||||||
|
int product_id FK
|
||||||
|
varchar label_snapshot
|
||||||
|
}
|
||||||
|
order_item_modifier {
|
||||||
|
int id PK
|
||||||
|
int order_item_id FK
|
||||||
|
int ingredient_id FK
|
||||||
|
enum action
|
||||||
|
int extra_price_cents
|
||||||
|
}
|
||||||
|
product {
|
||||||
|
int id PK
|
||||||
|
varchar name
|
||||||
|
}
|
||||||
|
menu {
|
||||||
|
int id PK
|
||||||
|
varchar name
|
||||||
|
}
|
||||||
|
menu_slot {
|
||||||
|
int id PK
|
||||||
|
varchar name
|
||||||
|
}
|
||||||
|
ingredient {
|
||||||
|
int id PK
|
||||||
|
varchar name
|
||||||
|
}
|
||||||
|
|
||||||
|
customer_order ||--o{ order_item : "contains"
|
||||||
|
order_item }o--o| product : "references_product"
|
||||||
|
order_item }o--o| menu : "references_menu"
|
||||||
|
order_item ||--o{ order_item_selection : "fills_slot"
|
||||||
|
order_item ||--o{ order_item_modifier : "modifies_ingredient"
|
||||||
|
menu_slot ||--o{ order_item_selection : "slot_filled_by"
|
||||||
|
product ||--o{ order_item_selection : "chosen_for_slot"
|
||||||
|
ingredient ||--o{ order_item_modifier : "modified_by"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Association cardinalities
|
||||||
|
|
||||||
|
| # | Association | Side A | Cardinality A | Side B | Cardinality 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. |
|
||||||
|
|
||||||
|
### 6.3 Notes on the Order sub-domain
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**`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_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`.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
**`service_day` computation** (KPI grouping): not stored as a column. Computed at query time:
|
||||||
|
```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).
|
||||||
|
|
||||||
|
**`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).
|
||||||
|
|
||||||
|
**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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Sub-domain: RBAC
|
||||||
|
|
||||||
|
### 7.1 Mermaid entity-relationship diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
user {
|
||||||
|
int id PK
|
||||||
|
varchar email
|
||||||
|
varchar password_hash
|
||||||
|
varchar first_name
|
||||||
|
varchar last_name
|
||||||
|
int role_id FK
|
||||||
|
tinyint is_active
|
||||||
|
datetime last_login_at
|
||||||
|
}
|
||||||
|
role {
|
||||||
|
int id PK
|
||||||
|
varchar code
|
||||||
|
varchar label
|
||||||
|
text description
|
||||||
|
varchar default_route
|
||||||
|
enum order_source
|
||||||
|
tinyint is_active
|
||||||
|
}
|
||||||
|
role_visible_source {
|
||||||
|
int role_id FK
|
||||||
|
enum source
|
||||||
|
}
|
||||||
|
permission {
|
||||||
|
int id PK
|
||||||
|
varchar code
|
||||||
|
varchar label
|
||||||
|
text description
|
||||||
|
}
|
||||||
|
role_permission {
|
||||||
|
int role_id FK
|
||||||
|
int permission_id FK
|
||||||
|
}
|
||||||
|
|
||||||
|
user }o--|| role : "holds"
|
||||||
|
role ||--o{ role_visible_source : "sees_source"
|
||||||
|
role ||--o{ role_permission : "grants"
|
||||||
|
permission ||--o{ role_permission : "granted_to"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 Association cardinalities
|
||||||
|
|
||||||
|
| # | Association | Side A | Cardinality A | Side B | Cardinality 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. |
|
||||||
|
|
||||||
|
### 7.3 Notes on the RBAC sub-domain
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
**`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.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_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_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`.
|
||||||
|
|
||||||
|
**Seed roles** (5 roles, frozen at DDL; extendable without code change):
|
||||||
|
`admin`, `manager`, `kitchen`, `counter`, `drive`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Cross-validation MCD <-> dictionary
|
||||||
|
|
||||||
|
Verification that all 19 dictionary entities appear in the MCD and vice versa.
|
||||||
|
|
||||||
|
| # | Dictionary entity (section 3) | Sub-domain in MCD | Present |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
**Result**: 19/19 entities traced. No entity from the dictionary is absent from the MCD.
|
||||||
|
No entity in the MCD falls outside the dictionary.
|
||||||
|
|
||||||
|
**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)
|
||||||
|
- `user`: RBAC (authentication) + Ingredients (stock movement author)
|
||||||
|
|
||||||
|
This is expected in a normalised model. The sub-domain split is for readability; the actual
|
||||||
|
relational schema is a unified graph.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Decisions deferred to the MLD
|
||||||
|
|
||||||
|
The MCD remains at the conceptual level. The following decisions are deferred to the 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. MCD <-> MCT coherence (mantra #34)
|
||||||
|
|
||||||
|
Pre-validation: each entity participates in at least one treatment.
|
||||||
|
|
||||||
|
| Entity | Expected treatment(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 |
|
||||||
|
|
||||||
|
Cross-validation MCD <-> MCT (mantra #34) to be completed exhaustively in `mct.md`
|
||||||
|
once the MCT is updated to the 4-state machine and 19-entity model.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Note on .drawio diagram regeneration
|
||||||
|
|
||||||
|
The `.drawio` XML sources in `docs/merise/_diagrams/` reflect the v0.1 model (11 entities,
|
||||||
|
French naming). They are scheduled for regeneration from this v0.2 MCD as a separate task.
|
||||||
|
Until regenerated, this Markdown document is the authoritative conceptual model. The Mermaid
|
||||||
|
`erDiagram` blocks in sections 4-7 render natively on GitHub and serve as the interim
|
||||||
|
graphical reference.
|
||||||
611
docs/merise/mct.md
Normal file
611
docs/merise/mct.md
Normal file
|
|
@ -0,0 +1,611 @@
|
||||||
|
# Model of Conceptual Treatments (MCT) — Wakdo
|
||||||
|
|
||||||
|
**Merise phase** : P1 - Conception, step 3 (after MCD)
|
||||||
|
**Version** : v0.2 — prod-like, 4-state machine
|
||||||
|
**Date** : 2026-06-04
|
||||||
|
**Branch** : `feat/p1-conception`
|
||||||
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
The MCT (Model of Conceptual Treatments) describes the **business operations** of the Wakdo
|
||||||
|
domain in the canonical Merise form: **triggering event -> operation -> emitted result**.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
**Identified actors**:
|
||||||
|
|
||||||
|
| Actor | 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) |
|
||||||
|
| Manager | MANAGER | Back-office, role `manager` |
|
||||||
|
| Administrator | ADMIN | Back-office, role `admin` |
|
||||||
|
| System | SYS | Internal API / PHP logic |
|
||||||
|
|
||||||
|
**MCD cross-reference**: each operation references entities from the MCD (section 14).
|
||||||
|
The MCT is consistent with the `customer_order.status` state machine:
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Representation conventions
|
||||||
|
|
||||||
|
### Operation format
|
||||||
|
|
||||||
|
```
|
||||||
|
[TRIGGERING EVENT(S)]
|
||||||
|
|
|
||||||
|
| [SYNCHRONISATION RULE / CONDITION]
|
||||||
|
v
|
||||||
|
( OPERATION )
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[EMITTED RESULT(S)]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Synchronisations**:
|
||||||
|
- `AND`: all events must be present simultaneously to trigger the operation.
|
||||||
|
- `OR`: any one of the events is sufficient.
|
||||||
|
|
||||||
|
**Conditions**: expressed in square brackets `[condition]` on the incoming arc.
|
||||||
|
|
||||||
|
### Textual notation
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Domain 1 — Order lifecycle (kiosk)
|
||||||
|
|
||||||
|
### 3.1 LOAD_CATALOGUE
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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) |
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 COMPOSE_CART
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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` |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 CREATE_ORDER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 DISPLAY_CONFIRMATION
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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` |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Domain 2 — Order lifecycle (counter and drive)
|
||||||
|
|
||||||
|
### 4.1 CREATE_COUNTER_ORDER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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`). |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Domain 3 — Preparation display (kitchen)
|
||||||
|
|
||||||
|
### 5.1 LIST_ORDERS_DISPLAY
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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`. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Domain 4 — Delivery to customer
|
||||||
|
|
||||||
|
### 6.1 DELIVER_ORDER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Triggering events** | 1. The order is at status `paid` AND 2. Counter or drive staff clicks "Delivered" |
|
||||||
|
| **Actor** | COUNTER or 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). |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Domain 5 — Cancellation
|
||||||
|
|
||||||
|
### 7.1 CANCEL_ORDER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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`. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Domain 6 — Catalogue management
|
||||||
|
|
||||||
|
### 8.1 CREATE_PRODUCT
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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`. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.2 UPDATE_PRODUCT
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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). |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.3 DELETE_PRODUCT
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.4 CREATE_MENU
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.5 UPDATE_MENU
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.6 DELETE_MENU
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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" |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.7 MANAGE_CATEGORY
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.8 MANAGE_INGREDIENT
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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`. |
|
||||||
|
| **Operation** | MANAGE_INGREDIENT |
|
||||||
|
| **Description** | CRUD on `ingredient` (name, unit, pack_size, pack_label, low_stock_threshold, 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Domain 7 — Stock management
|
||||||
|
|
||||||
|
### 9.1 RESTOCK
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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`. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.2 INVENTORY_COUNT
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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`. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.3 READ_STOCK
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Triggering event** | An authorised actor accesses the stock view |
|
||||||
|
| **Actor** | KITCHEN, COUNTER, DRIVE, MANAGER, or ADMIN |
|
||||||
|
| **Synchronisation** | None |
|
||||||
|
| **Condition** | Actor holds permission `stock.read`. |
|
||||||
|
| **Operation** | READ_STOCK |
|
||||||
|
| **Description** | Read `ingredient` list with current `stock_quantity`, `low_stock_threshold`, `pack_size`, `pack_label`. Low-stock alert computed at display time: `stock_quantity <= low_stock_threshold`. 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Domain 8 — User and role management (admin)
|
||||||
|
|
||||||
|
### 10.1 CREATE_USER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.2 UPDATE_USER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.3 DEACTIVATE_USER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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). |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.4 MANAGE_RBAC
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **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. |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Domain 9 — Stats and KPI
|
||||||
|
|
||||||
|
### 11.1 READ_STATS
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Triggering event** | Manager or admin accesses the stats dashboard |
|
||||||
|
| **Actor** | MANAGER or ADMIN |
|
||||||
|
| **Synchronisation** | None |
|
||||||
|
| **Condition** | Actor holds 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Domain 10 — Back-office authentication
|
||||||
|
|
||||||
|
### 12.1 AUTHENTICATE_USER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Triggering event** | An actor submits the login form |
|
||||||
|
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
|
||||||
|
| **Synchronisation** | None |
|
||||||
|
| **Condition** | Email exists in database. Password matches argon2id hash. User `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`. Idle timeout: 4h. Absolute timeout: 10h. Redirect to `role.default_route`. |
|
||||||
|
| **MCD entities** | R: `user` (verification), `role` (load permissions, default_route), `role_permission` — W: `user` (UPDATE last_login_at) |
|
||||||
|
| **Result** | Session opened, redirect to role-specific default view |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12.2 LOGOUT_USER
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| **Triggering event** | Actor clicks "Logout" OR session expires |
|
||||||
|
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN / SYS (expiry) |
|
||||||
|
| **Synchronisation** | OR |
|
||||||
|
| **Condition** | A valid session is open |
|
||||||
|
| **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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. State machine — customer_order.status
|
||||||
|
|
||||||
|
Summary of transitions covered by MCT operations.
|
||||||
|
|
||||||
|
```
|
||||||
|
[CUSTOMER / COUNTER / DRIVE]
|
||||||
|
CREATE_ORDER
|
||||||
|
CREATE_COUNTER_ORDER
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ pending_payment ] (order composed, payment pending)
|
||||||
|
|
|
||||||
|
[CUSTOMER / COUNTER / DRIVE] payment confirmed
|
||||||
|
(atomic within CREATE_ORDER / CREATE_COUNTER_ORDER)
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ paid ]
|
||||||
|
|
|
||||||
|
[COUNTER / DRIVE] DELIVER_ORDER
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ delivered ] (terminal, cannot be cancelled)
|
||||||
|
|
||||||
|
|
||||||
|
From pending_payment / paid:
|
||||||
|
[COUNTER, DRIVE, or ADMIN] CANCEL_ORDER
|
||||||
|
|
|
||||||
|
v
|
||||||
|
[ 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Operations summary table
|
||||||
|
|
||||||
|
| # | Operation | Domain | Actor | W Entities | R Entities |
|
||||||
|
|---|-----------|--------|-------|------------|------------|
|
||||||
|
| 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 |
|
||||||
|
| 3 | CREATE_ORDER | Order kiosk | CUSTOMER | customer_order, order_item, order_item_selection, order_item_modifier, ingredient, stock_movement | product, menu, ingredient, product_ingredient |
|
||||||
|
| 4 | DISPLAY_CONFIRMATION | Order kiosk | SYS | — | — |
|
||||||
|
| 5 | CREATE_COUNTER_ORDER | Order counter/drive | COUNTER/DRIVE | customer_order, order_item, order_item_selection, order_item_modifier, ingredient, stock_movement | product, menu, menu_slot, menu_slot_option, ingredient, product_ingredient |
|
||||||
|
| 6 | LIST_ORDERS_DISPLAY | Preparation | KITCHEN/COUNTER/DRIVE/ADMIN | — | customer_order, order_item, order_item_selection, order_item_modifier, role_visible_source |
|
||||||
|
| 7 | DELIVER_ORDER | Delivery | COUNTER/DRIVE | customer_order | — |
|
||||||
|
| 8 | CANCEL_ORDER | Cancellation | COUNTER/DRIVE/ADMIN | customer_order, ingredient, stock_movement | order_item, order_item_modifier, ingredient, product_ingredient |
|
||||||
|
| 9 | CREATE_PRODUCT | Catalogue | ADMIN/MANAGER | product | category |
|
||||||
|
| 10 | UPDATE_PRODUCT | Catalogue | ADMIN/MANAGER | product | — |
|
||||||
|
| 11 | DELETE_PRODUCT | Catalogue | ADMIN | product | menu_slot_option, order_item, menu |
|
||||||
|
| 12 | CREATE_MENU | Catalogue | ADMIN/MANAGER | menu, menu_slot, menu_slot_option | product, category |
|
||||||
|
| 13 | UPDATE_MENU | Catalogue | ADMIN/MANAGER | menu, menu_slot, menu_slot_option | — |
|
||||||
|
| 14 | DELETE_MENU | Catalogue | ADMIN | menu_slot_option, menu_slot, menu | order_item |
|
||||||
|
| 15 | MANAGE_CATEGORY | Catalogue | ADMIN/MANAGER | category | product, menu |
|
||||||
|
| 16 | MANAGE_INGREDIENT | Catalogue | ADMIN/MANAGER | ingredient, product_ingredient, ingredient_allergen | product, allergen |
|
||||||
|
| 17 | RESTOCK | Stock | MANAGER/ADMIN | ingredient, stock_movement | ingredient |
|
||||||
|
| 18 | INVENTORY_COUNT | Stock | KITCHEN/COUNTER/DRIVE/MANAGER/ADMIN | ingredient, stock_movement | ingredient |
|
||||||
|
| 19 | READ_STOCK | Stock | KITCHEN/COUNTER/DRIVE/MANAGER/ADMIN | — | ingredient, stock_movement |
|
||||||
|
| 20 | CREATE_USER | RBAC | ADMIN | user | role |
|
||||||
|
| 21 | UPDATE_USER | RBAC | ADMIN | user | — |
|
||||||
|
| 22 | DEACTIVATE_USER | RBAC | ADMIN | user | — |
|
||||||
|
| 23 | MANAGE_RBAC | RBAC | ADMIN | role_permission, role, role_visible_source | role, permission |
|
||||||
|
| 24 | READ_STATS | Stats | MANAGER/ADMIN | — | customer_order, order_item |
|
||||||
|
| 25 | AUTHENTICATE_USER | Auth | ALL BACK | user | user, role, role_permission |
|
||||||
|
| 26 | LOGOUT_USER | Auth | ALL BACK | — | — |
|
||||||
|
|
||||||
|
**Total: 26 operations** covering the complete Wakdo business lifecycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. MCT -> MCD cross-validation (mantra #34)
|
||||||
|
|
||||||
|
Verification that each MCD entity participates in at least one MCT operation.
|
||||||
|
|
||||||
|
| MCD entity | Operations that read | Operations that write | Coverage |
|
||||||
|
|------------|---------------------|----------------------|----------|
|
||||||
|
| `category` | 1, 9, 12, 15 | 15 | OK |
|
||||||
|
| `product` | 1, 2, 3, 5, 9, 11, 12 | 9, 10, 11 | OK |
|
||||||
|
| `menu` | 1, 2, 3, 5, 12, 14 | 12, 13, 14 | OK |
|
||||||
|
| `menu_slot` | 1, 2, 5 | 12, 13, 14 | OK |
|
||||||
|
| `menu_slot_option` | 1, 2, 5, 11 | 12, 13, 14 | OK |
|
||||||
|
| `ingredient` | 1, 2, 3, 5, 8, 16, 17, 18, 19 | 3, 5, 8, 16, 17, 18 | OK |
|
||||||
|
| `product_ingredient` | 2, 3, 5, 8 | 16 | OK |
|
||||||
|
| `allergen` | 1 | — (static seed) | OK (*) |
|
||||||
|
| `ingredient_allergen` | 1 | 16 | OK |
|
||||||
|
| `customer_order` | 6, 8, 24 | 3, 5, 7, 8 | OK |
|
||||||
|
| `order_item` | 6, 8, 14, 24 | 3, 5 | OK |
|
||||||
|
| `order_item_selection` | 6 | 3, 5 | OK |
|
||||||
|
| `order_item_modifier` | 6, 8 | 3, 5 | OK |
|
||||||
|
| `user` | 25 | 20, 21, 22, 25 | OK |
|
||||||
|
| `role` | 20, 23, 25 | 23 | OK |
|
||||||
|
| `role_visible_source` | 6 | 23 | OK |
|
||||||
|
| `permission` | 23 | — (static seed) | OK (*) |
|
||||||
|
| `role_permission` | 25 | 23 | OK |
|
||||||
|
| `stock_movement` | 19 | 3, 5, 8, 17, 18 | 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.
|
||||||
|
|
||||||
|
**Conclusion**: 19/19 entities covered. MCT <-> MCD consistency validated.
|
||||||
906
docs/merise/mld.md
Normal file
906
docs/merise/mld.md
Normal file
|
|
@ -0,0 +1,906 @@
|
||||||
|
# Logical Data Model (MLD) — Wakdo
|
||||||
|
|
||||||
|
**Merise phase** : P1 - Conception, step 5 (after MCD, MCT, MLT)
|
||||||
|
**Version** : v0.2 — prod-like, 19 tables
|
||||||
|
**Date** : 2026-06-04
|
||||||
|
**Branch** : `feat/p1-conception`
|
||||||
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose of this document
|
||||||
|
|
||||||
|
The MLD transcribes the MCD into a formal relational schema: 1 entity -> 1 table, each
|
||||||
|
association translated according to its cardinality, referential constraints materialised,
|
||||||
|
indexes sized for frequent access patterns.
|
||||||
|
|
||||||
|
This is the step that transforms conceptual modelling into an implementable specification.
|
||||||
|
The DDL SQL (`db/migrations/0001_init_schema.sql`) will be derived directly from this
|
||||||
|
document at P2.
|
||||||
|
|
||||||
|
**Sources**:
|
||||||
|
- `docs/merise/dictionary.md` (v0.2 — types and constraints per attribute, source of truth)
|
||||||
|
- `docs/merise/mcd.md` (v0.2 — entities + cardinalities + deferred decisions)
|
||||||
|
- `docs/notes/revue-alignement-p1.md` §7 (decision table D1-D8 + stock)
|
||||||
|
|
||||||
|
**Target platform**:
|
||||||
|
- MariaDB 11.4 LTS (cf. `docker-compose.yml` service `wakdo-db`)
|
||||||
|
- Engine InnoDB (ACID, FK support, row-level locking, CHECK from 10.2.1)
|
||||||
|
- Charset `utf8mb4`, collation `utf8mb4_unicode_ci`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Notation conventions
|
||||||
|
|
||||||
|
### Relational notation
|
||||||
|
|
||||||
|
```
|
||||||
|
table_name (col1, col2, #col_fk, [col_nullable])
|
||||||
|
|
||||||
|
PK : col1
|
||||||
|
UK : col2
|
||||||
|
FK : col_fk -> other_table(id) ON DELETE <rule>
|
||||||
|
IDX : (col_a, col_b)
|
||||||
|
CHK : <expression>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Symbol | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `col` | NOT NULL column |
|
||||||
|
| `[col]` | Nullable column |
|
||||||
|
| `#col` | FK column |
|
||||||
|
|
||||||
|
Notation follows Merise French usage (Nanci/Espinasse convention adapted for ASCII).
|
||||||
|
|
||||||
|
### Type summary
|
||||||
|
|
||||||
|
All exact types are defined in `dictionary.md` section 2. Conventions retained:
|
||||||
|
- `INT UNSIGNED AUTO_INCREMENT` for all technical PKs
|
||||||
|
- `INT UNSIGNED` for all monetary amounts in cents (anti-FLOAT, see dictionary note 1)
|
||||||
|
- `SMALLINT UNSIGNED` for `vat_rate` per-mille values (55 or 100)
|
||||||
|
- `ENUM(...)` for stable business values (see dictionary note 2)
|
||||||
|
- `DATETIME` for timestamps (not TIMESTAMP, which implicitly converts to UTC in MariaDB)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. MCD -> MLD translation rules applied
|
||||||
|
|
||||||
|
### 3.1 Entity -> Table
|
||||||
|
|
||||||
|
Each MCD entity becomes one table. The conceptual identifier `id` becomes PK
|
||||||
|
`INT UNSIGNED AUTO_INCREMENT`. Attributes retain their names and types.
|
||||||
|
|
||||||
|
### 3.2 `(1,1) - (1,N)` association -> simple FK
|
||||||
|
|
||||||
|
The entity on the `(1,1)` side carries the FK toward the `(0,N)` or `(1,N)` entity.
|
||||||
|
|
||||||
|
### 3.3 `(0,N) - (0,N)` or `(1,N) - (1,N)` association -> join table
|
||||||
|
|
||||||
|
The association becomes its own table with a composite PK of the two FKs. Applied to:
|
||||||
|
`product_ingredient`, `menu_slot_option`, `ingredient_allergen`,
|
||||||
|
`role_visible_source`, `role_permission`.
|
||||||
|
|
||||||
|
### 3.4 Associative entity with own attributes -> join table with columns
|
||||||
|
|
||||||
|
When an N-N association carries its own attributes, it becomes a table with those attributes
|
||||||
|
in addition to the composite FK PK. Applied to `product_ingredient`.
|
||||||
|
|
||||||
|
### 3.5 Polymorphism -> 2 nullable FKs + discriminator + CHECK
|
||||||
|
|
||||||
|
`order_item` references either `product` or `menu`. Translated as 2 nullable FK columns +
|
||||||
|
1 discriminator ENUM + 1 CHECK constraint enforcing mutual exclusivity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Relational schema (19 tables)
|
||||||
|
|
||||||
|
Tables are ordered by dependency (no-FK tables first, then tables that depend on them).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.1 `category`
|
||||||
|
|
||||||
|
```
|
||||||
|
category (id, name, slug, [image_path], display_order, is_active, created_at, updated_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
UK : name
|
||||||
|
UK : slug
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `name` | VARCHAR(60) | NO | Unique display name (see dict 3.1) |
|
||||||
|
| `slug` | VARCHAR(60) | NO | URL slug, e.g. `burgers` |
|
||||||
|
| `image_path` | VARCHAR(255) | YES | Relative path from public root |
|
||||||
|
| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Kiosk display order |
|
||||||
|
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Soft deactivation |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
No FK. Root table for the Catalogue sub-domain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.2 `product`
|
||||||
|
|
||||||
|
```
|
||||||
|
product (id, #category_id, name, [description], price_cents, vat_rate,
|
||||||
|
[image_path], is_available, display_order, created_at, updated_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
FK : category_id -> category(id) ON DELETE RESTRICT
|
||||||
|
IDX : (category_id, is_available, display_order)
|
||||||
|
CHK : price_cents > 0
|
||||||
|
CHK : vat_rate IN (55, 100)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `category_id` | INT UNSIGNED | NO | FK -> category |
|
||||||
|
| `name` | VARCHAR(120) | NO | Product label |
|
||||||
|
| `description` | TEXT | YES | Optional long description |
|
||||||
|
| `price_cents` | INT UNSIGNED | NO | A la carte price, incl. VAT, in cents |
|
||||||
|
| `vat_rate` | SMALLINT UNSIGNED | NO | Per-mille: 100 = 10%, 55 = 5.5% |
|
||||||
|
| `image_path` | VARCHAR(255) | YES | Relative path from public root |
|
||||||
|
| `is_available` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Manual availability toggle |
|
||||||
|
| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Within-category display order |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
**ON DELETE RESTRICT** on `category_id`: a category with products cannot be deleted. Prevents
|
||||||
|
orphaned products.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.3 `menu`
|
||||||
|
|
||||||
|
```
|
||||||
|
menu (id, #category_id, #burger_product_id, name, [description],
|
||||||
|
price_normal_cents, price_maxi_cents, [image_path],
|
||||||
|
is_available, display_order, created_at, updated_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
FK : category_id -> category(id) ON DELETE RESTRICT
|
||||||
|
FK : burger_product_id -> product(id) ON DELETE RESTRICT
|
||||||
|
IDX : (category_id, is_available, display_order)
|
||||||
|
CHK : price_normal_cents > 0
|
||||||
|
CHK : price_maxi_cents > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `category_id` | INT UNSIGNED | NO | FK -> category (typically the `menus` category) |
|
||||||
|
| `burger_product_id` | INT UNSIGNED | NO | FK -> product — the fixed burger that anchors this menu |
|
||||||
|
| `name` | VARCHAR(120) | NO | e.g. "Menu Le 280" |
|
||||||
|
| `description` | TEXT | YES | Optional |
|
||||||
|
| `price_normal_cents` | INT UNSIGNED | NO | Normal format price in cents |
|
||||||
|
| `price_maxi_cents` | INT UNSIGNED | NO | Maxi format price in cents (~+150 cents) |
|
||||||
|
| `image_path` | VARCHAR(255) | YES | Typically reuses the burger image |
|
||||||
|
| `is_available` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Availability toggle |
|
||||||
|
| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Display order |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
**ON DELETE RESTRICT** on both FKs: prevents deletion of a category or burger product that
|
||||||
|
is still referenced by a menu definition.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4 `menu_slot`
|
||||||
|
|
||||||
|
```
|
||||||
|
menu_slot (id, #menu_id, name, slot_type, is_required, display_order)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
FK : menu_id -> menu(id) ON DELETE CASCADE
|
||||||
|
IDX : (menu_id, display_order)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `menu_id` | INT UNSIGNED | NO | FK -> menu |
|
||||||
|
| `name` | VARCHAR(80) | NO | e.g. "Drink", "Side", "Sauce" |
|
||||||
|
| `slot_type` | ENUM('drink','side','sauce','dessert','extra') | NO | Semantic role |
|
||||||
|
| `is_required` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Whether the customer must fill this slot |
|
||||||
|
| `display_order` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Display order within menu builder |
|
||||||
|
|
||||||
|
**No audit fields**: a slot is part of menu definition; created and updated together with
|
||||||
|
the menu.
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `menu_id`: if a menu is deleted, its slots are deleted with it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.5 `menu_slot_option`
|
||||||
|
|
||||||
|
Pure join table. Composite PK.
|
||||||
|
|
||||||
|
```
|
||||||
|
menu_slot_option (#menu_slot_id, #product_id)
|
||||||
|
|
||||||
|
PK : (menu_slot_id, product_id)
|
||||||
|
FK : menu_slot_id -> menu_slot(id) ON DELETE CASCADE
|
||||||
|
FK : product_id -> product(id) ON DELETE RESTRICT
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `menu_slot_id` | INT UNSIGNED | NO | FK -> menu_slot |
|
||||||
|
| `product_id` | INT UNSIGNED | NO | FK -> product |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `menu_slot_id`: if a slot is deleted, its eligibility list goes with it.
|
||||||
|
**ON DELETE RESTRICT** on `product_id`: a product listed as eligible in a slot cannot be
|
||||||
|
deleted without first removing it from the slot options. Prevents silent breakage of menus.
|
||||||
|
|
||||||
|
No timestamps. Pure join table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.6 `ingredient`
|
||||||
|
|
||||||
|
```
|
||||||
|
ingredient (id, name, unit, stock_quantity, pack_size, [pack_label],
|
||||||
|
low_stock_threshold, is_active, created_at, updated_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
UK : name
|
||||||
|
CHK : stock_quantity >= 0
|
||||||
|
CHK : pack_size > 0
|
||||||
|
CHK : low_stock_threshold >= 0
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `name` | VARCHAR(120) | NO | Unique name, e.g. "Sesame Bun" |
|
||||||
|
| `unit` | VARCHAR(40) | NO | Packaging unit label (free-form, not ENUM) |
|
||||||
|
| `stock_quantity` | INT NOT NULL DEFAULT 0 | NO | Current stock. Signed INT to detect negative (alert) |
|
||||||
|
| `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units per restocking pack |
|
||||||
|
| `pack_label` | VARCHAR(80) | YES | Human label of the pack |
|
||||||
|
| `low_stock_threshold` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Alert threshold |
|
||||||
|
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivate obsolete ingredients |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
No FK. Root table for the Ingredients & Stock sub-domain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.7 `product_ingredient`
|
||||||
|
|
||||||
|
Associative table carrying recipe and customisation metadata. Composite PK.
|
||||||
|
|
||||||
|
```
|
||||||
|
product_ingredient (#product_id, #ingredient_id, quantity_normal, quantity_maxi,
|
||||||
|
is_removable, is_addable, extra_price_cents)
|
||||||
|
|
||||||
|
PK : (product_id, ingredient_id)
|
||||||
|
FK : product_id -> product(id) ON DELETE CASCADE
|
||||||
|
FK : ingredient_id -> ingredient(id) ON DELETE RESTRICT
|
||||||
|
CHK : quantity_normal > 0
|
||||||
|
CHK : quantity_maxi >= quantity_normal
|
||||||
|
CHK : extra_price_cents >= 0
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `product_id` | INT UNSIGNED | NO | FK -> product |
|
||||||
|
| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient |
|
||||||
|
| `quantity_normal` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units consumed in Normal format |
|
||||||
|
| `quantity_maxi` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units consumed in Maxi format; equals `quantity_normal` for burger/sauce (format-invariant), higher for side/drink |
|
||||||
|
| `is_removable` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Customer may remove at no cost |
|
||||||
|
| `is_addable` | TINYINT(1) NOT NULL DEFAULT 0 | NO | Customer may add an extra unit |
|
||||||
|
| `extra_price_cents` | INT UNSIGNED NOT NULL DEFAULT 0 | NO | Surcharge if `is_addable=1` and customer adds it |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `product_id`: if a product is deleted, its recipe rows are deleted.
|
||||||
|
**ON DELETE RESTRICT** on `ingredient_id`: cannot delete an ingredient still referenced in a
|
||||||
|
recipe. Admin must remove the product-ingredient link first.
|
||||||
|
|
||||||
|
No timestamps. Join table with attributes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.8 `allergen`
|
||||||
|
|
||||||
|
```
|
||||||
|
allergen (id, code, name, [description])
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
UK : code
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `code` | VARCHAR(30) | NO | Machine code, e.g. `gluten`, `milk` |
|
||||||
|
| `name` | VARCHAR(80) | NO | Display name |
|
||||||
|
| `description` | TEXT | YES | Optional guidance |
|
||||||
|
|
||||||
|
No FK. Reference table; 14 rows at seed (INCO Regulation (EU) 1169/2011).
|
||||||
|
No `updated_at`: allergen catalogue is considered stable (additions require a migration, not a UI action).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.9 `ingredient_allergen`
|
||||||
|
|
||||||
|
Pure join table. Composite PK.
|
||||||
|
|
||||||
|
```
|
||||||
|
ingredient_allergen (#ingredient_id, #allergen_id)
|
||||||
|
|
||||||
|
PK : (ingredient_id, allergen_id)
|
||||||
|
FK : ingredient_id -> ingredient(id) ON DELETE CASCADE
|
||||||
|
FK : allergen_id -> allergen(id) ON DELETE RESTRICT
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient |
|
||||||
|
| `allergen_id` | INT UNSIGNED | NO | FK -> allergen |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `ingredient_id`: if an ingredient is deleted, its allergen links go with it.
|
||||||
|
**ON DELETE RESTRICT** on `allergen_id`: an allergen in the regulated catalogue cannot be deleted.
|
||||||
|
|
||||||
|
No timestamps. Pure join table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.10 `role`
|
||||||
|
|
||||||
|
Placed before `user` because `user` depends on `role`.
|
||||||
|
|
||||||
|
```
|
||||||
|
role (id, code, label, [description], [default_route], [order_source],
|
||||||
|
is_active, created_at, updated_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
UK : code
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `code` | VARCHAR(40) | NO | Machine code: `admin`, `manager`, `kitchen`, `counter`, `drive` |
|
||||||
|
| `label` | VARCHAR(80) | NO | Display name |
|
||||||
|
| `description` | TEXT | YES | Optional |
|
||||||
|
| `default_route` | VARCHAR(120) | YES | Landing screen, e.g. `/admin/dashboard` |
|
||||||
|
| `order_source` | ENUM('kiosk','counter','drive') | YES | Auto-tagged source when this role creates an order; NULL for admin/manager |
|
||||||
|
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivation preserves history |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
No FK. Root table for RBAC.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.11 `user`
|
||||||
|
|
||||||
|
```
|
||||||
|
user (id, email, password_hash, first_name, last_name, #role_id,
|
||||||
|
is_active, [last_login_at], created_at, updated_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
UK : email
|
||||||
|
FK : role_id -> role(id) ON DELETE RESTRICT
|
||||||
|
IDX : (is_active, role_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `email` | VARCHAR(254) | NO | RFC 5321 max length |
|
||||||
|
| `password_hash` | VARCHAR(255) | NO | argon2id hash |
|
||||||
|
| `first_name` | VARCHAR(60) | NO | |
|
||||||
|
| `last_name` | VARCHAR(60) | NO | |
|
||||||
|
| `role_id` | INT UNSIGNED | NO | FK -> role |
|
||||||
|
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivation without deletion |
|
||||||
|
| `last_login_at` | DATETIME | YES | Audit, dormant account detection |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
**ON DELETE RESTRICT** on `role_id`: a role cannot be deleted while users hold it.
|
||||||
|
Deactivate the role first (`is_active = 0`), then reassign users before deleting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.12 `role_visible_source`
|
||||||
|
|
||||||
|
Pure join table. Composite PK.
|
||||||
|
|
||||||
|
```
|
||||||
|
role_visible_source (#role_id, source)
|
||||||
|
|
||||||
|
PK : (role_id, source)
|
||||||
|
FK : role_id -> role(id) ON DELETE CASCADE
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `role_id` | INT UNSIGNED | NO | FK -> role |
|
||||||
|
| `source` | ENUM('kiosk','counter','drive') | NO | Order source visible on dashboard |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `role_id`: if a role is deleted, its dashboard source filters go with it.
|
||||||
|
|
||||||
|
No timestamps. Pure join table.
|
||||||
|
|
||||||
|
Seed data:
|
||||||
|
- `kitchen`: kiosk, counter, drive
|
||||||
|
- `counter`: kiosk, counter
|
||||||
|
- `drive`: drive
|
||||||
|
- `admin`, `manager`: no rows (global view, no source filter)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.13 `permission`
|
||||||
|
|
||||||
|
```
|
||||||
|
permission (id, code, label, [description], created_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
UK : code
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `code` | VARCHAR(60) | NO | Format `<resource>.<action>` |
|
||||||
|
| `label` | VARCHAR(120) | NO | Display name |
|
||||||
|
| `description` | TEXT | YES | Optional |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
No `updated_at`: permissions are declared in migration and not modified via UI.
|
||||||
|
Catalogue is frozen at 23 codes (see dictionary section 3.17).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.14 `role_permission`
|
||||||
|
|
||||||
|
Pure join table. Composite PK.
|
||||||
|
|
||||||
|
```
|
||||||
|
role_permission (#role_id, #permission_id)
|
||||||
|
|
||||||
|
PK : (role_id, permission_id)
|
||||||
|
FK : role_id -> role(id) ON DELETE CASCADE
|
||||||
|
FK : permission_id -> permission(id) ON DELETE CASCADE
|
||||||
|
IDX : permission_id
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `role_id` | INT UNSIGNED | NO | FK -> role |
|
||||||
|
| `permission_id` | INT UNSIGNED | NO | FK -> permission |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on both FKs: deleting a role or a permission removes its mappings.
|
||||||
|
The secondary index on `permission_id` supports the reverse query "which roles have this
|
||||||
|
permission?" without scanning the full table.
|
||||||
|
|
||||||
|
No timestamps. Pure join table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.15 `customer_order`
|
||||||
|
|
||||||
|
```
|
||||||
|
customer_order (id, order_number, source, service_mode, status,
|
||||||
|
total_ht_cents, total_vat_cents, total_ttc_cents,
|
||||||
|
[paid_at], [delivered_at], [cancelled_at],
|
||||||
|
created_at, updated_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
UK : order_number
|
||||||
|
IDX : (status, created_at)
|
||||||
|
IDX : (source, created_at)
|
||||||
|
IDX : created_at
|
||||||
|
CHK : total_ht_cents >= 0
|
||||||
|
CHK : total_vat_cents >= 0
|
||||||
|
CHK : total_ttc_cents > 0
|
||||||
|
CHK : total_ttc_cents = total_ht_cents + total_vat_cents
|
||||||
|
CHK : source != 'drive' OR service_mode = 'drive'
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `order_number` | VARCHAR(20) | NO | Format `K/C/D-YYYY-MM-DD-NNN` by channel |
|
||||||
|
| `source` | ENUM('kiosk','counter','drive') | NO | Input channel |
|
||||||
|
| `service_mode` | ENUM('dine_in','takeaway','drive') | NO | Consumption mode (stats only, no fiscal role) |
|
||||||
|
| `status` | ENUM('pending_payment','paid','delivered','cancelled') NOT NULL DEFAULT 'pending_payment' | NO | 4-state machine |
|
||||||
|
| `total_ht_cents` | INT UNSIGNED | NO | Ex-VAT total snapshot |
|
||||||
|
| `total_vat_cents` | INT UNSIGNED | NO | VAT amount snapshot |
|
||||||
|
| `total_ttc_cents` | INT UNSIGNED | NO | Incl.-VAT total; must equal HT + VAT |
|
||||||
|
| `paid_at` | DATETIME | YES | Timestamp of transition to `paid` |
|
||||||
|
| `delivered_at` | DATETIME | YES | Timestamp of transition to `delivered` |
|
||||||
|
| `cancelled_at` | DATETIME | YES | Timestamp of cancellation |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Used as `service_day` base |
|
||||||
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
No FK toward `user`: staff attribution is not stored on the order. Operational accountability
|
||||||
|
is covered by `stock_movement.user_id` for stock actions.
|
||||||
|
|
||||||
|
**4-state machine**: `pending_payment -> paid -> delivered` (+ `cancelled`). States `preparing`
|
||||||
|
and `ready` are dropped (decision D4). KPI: `delivered_at - paid_at` (target SLA ~10 min).
|
||||||
|
|
||||||
|
**`service_day` computation** (used in stats queries — NOT a stored column):
|
||||||
|
```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 v0.1 was
|
||||||
|
incorrect and is dropped (decision D6).
|
||||||
|
|
||||||
|
**VAT calculation**: totals on `customer_order` are the sum of line-level calculations.
|
||||||
|
Line-level VAT: `unit_price_cents_snapshot * quantity` is the TTC amount per line;
|
||||||
|
HT = `ROUND(ttc_cents * 100 / (100 + vat_rate_per_cent))` where `vat_rate_per_cent`
|
||||||
|
is `vat_rate_snapshot / 10`. Computed at application layer at cart validation.
|
||||||
|
|
||||||
|
**`source = 'drive' => service_mode = 'drive'`**: the CHECK enforces this at DB level.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.16 `order_item`
|
||||||
|
|
||||||
|
```
|
||||||
|
order_item (id, #order_id, item_type, [#product_id], [#menu_id], format,
|
||||||
|
label_snapshot, unit_price_cents_snapshot, vat_rate_snapshot,
|
||||||
|
quantity, created_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
FK : order_id -> customer_order(id) ON DELETE CASCADE
|
||||||
|
FK : product_id -> product(id) ON DELETE RESTRICT
|
||||||
|
FK : menu_id -> menu(id) ON DELETE RESTRICT
|
||||||
|
IDX : order_id
|
||||||
|
CHK : unit_price_cents_snapshot > 0
|
||||||
|
CHK : vat_rate_snapshot IN (55, 100)
|
||||||
|
CHK : quantity > 0
|
||||||
|
CHK : (item_type = 'product' AND product_id IS NOT NULL AND menu_id IS NULL)
|
||||||
|
OR (item_type = 'menu' AND menu_id IS NOT NULL AND product_id IS NULL)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `order_id` | INT UNSIGNED | NO | FK -> customer_order |
|
||||||
|
| `item_type` | ENUM('product','menu') | NO | Discriminator |
|
||||||
|
| `product_id` | INT UNSIGNED | YES | Non-null if `item_type = 'product'`, NULL otherwise |
|
||||||
|
| `menu_id` | INT UNSIGNED | YES | Non-null if `item_type = 'menu'`, NULL otherwise |
|
||||||
|
| `format` | ENUM('normal','maxi') NOT NULL DEFAULT 'normal' | NO | Menu format. For standalone products, value is `normal` |
|
||||||
|
| `label_snapshot` | VARCHAR(120) | NO | Label at time of order |
|
||||||
|
| `unit_price_cents_snapshot` | INT UNSIGNED | NO | Unit price incl. VAT at time of order |
|
||||||
|
| `vat_rate_snapshot` | SMALLINT UNSIGNED | NO | VAT rate per-mille at time of order |
|
||||||
|
| `quantity` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Quantity (e.g. 3 drinks = 1 line, quantity=3) |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `order_id`: lines are deleted with the order.
|
||||||
|
**ON DELETE RESTRICT** on `product_id` and `menu_id`: a product or menu referenced in an
|
||||||
|
historical order line cannot be deleted. The snapshot makes the FK reference non-critical
|
||||||
|
for display, but RESTRICT avoids silent orphaning of the relational structure.
|
||||||
|
|
||||||
|
**Polymorphism exclusivity CHECK**: MariaDB 10.2+ enforces this at INSERT/UPDATE time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.17 `order_item_selection`
|
||||||
|
|
||||||
|
Customer's choice for one slot of a menu order line.
|
||||||
|
|
||||||
|
```
|
||||||
|
order_item_selection (id, #order_item_id, #menu_slot_id, #product_id, label_snapshot)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
FK : order_item_id -> order_item(id) ON DELETE CASCADE
|
||||||
|
FK : menu_slot_id -> menu_slot(id) ON DELETE RESTRICT
|
||||||
|
FK : product_id -> product(id) ON DELETE RESTRICT
|
||||||
|
IDX : order_item_id
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `order_item_id` | INT UNSIGNED | NO | FK -> order_item (must be a menu-type line) |
|
||||||
|
| `menu_slot_id` | INT UNSIGNED | NO | FK -> menu_slot (which slot was filled) |
|
||||||
|
| `product_id` | INT UNSIGNED | NO | FK -> product (chosen by customer) |
|
||||||
|
| `label_snapshot` | VARCHAR(120) | NO | Product label at time of order |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `order_item_id`: if the parent order line is deleted, its slot
|
||||||
|
selections go with it.
|
||||||
|
**ON DELETE RESTRICT** on `menu_slot_id` and `product_id`: historical slot choice records
|
||||||
|
must not be silently broken by catalogue changes.
|
||||||
|
|
||||||
|
Note: the business constraint that `order_item_id` references a line with `item_type='menu'`
|
||||||
|
is enforced at application layer (not in MariaDB without a trigger or deferred constraint).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.18 `order_item_modifier`
|
||||||
|
|
||||||
|
Ingredient-level modification applied by the customer to a product or the fixed burger of a menu.
|
||||||
|
|
||||||
|
```
|
||||||
|
order_item_modifier (id, #order_item_id, #ingredient_id, action, extra_price_cents)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
FK : order_item_id -> order_item(id) ON DELETE CASCADE
|
||||||
|
FK : ingredient_id -> ingredient(id) ON DELETE RESTRICT
|
||||||
|
IDX : order_item_id
|
||||||
|
CHK : extra_price_cents >= 0
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `order_item_id` | INT UNSIGNED | NO | FK -> order_item |
|
||||||
|
| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient |
|
||||||
|
| `action` | ENUM('remove','add') | NO | `remove` = free removal; `add` = extra unit |
|
||||||
|
| `extra_price_cents` | INT UNSIGNED NOT NULL DEFAULT 0 | NO | Snapshot of surcharge at time of order (0 for removals) |
|
||||||
|
|
||||||
|
**ON DELETE CASCADE** on `order_item_id`: if the order line is deleted, its modifiers go with it.
|
||||||
|
**ON DELETE RESTRICT** on `ingredient_id`: an ingredient referenced in a historical modifier
|
||||||
|
cannot be deleted.
|
||||||
|
|
||||||
|
**Modifier attachment for menu lines**: the modifiable product is the fixed burger, resolved
|
||||||
|
via `order_item.menu_id -> menu.burger_product_id`. No additional FK column is needed on
|
||||||
|
this table (see dictionary note 10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.19 `stock_movement`
|
||||||
|
|
||||||
|
Append-only audit log of all stock changes per ingredient.
|
||||||
|
|
||||||
|
```
|
||||||
|
stock_movement (id, #ingredient_id, movement_type, delta,
|
||||||
|
[#order_id], [#user_id], [note], created_at)
|
||||||
|
|
||||||
|
PK : id
|
||||||
|
FK : ingredient_id -> ingredient(id) ON DELETE RESTRICT
|
||||||
|
FK : order_id -> customer_order(id) ON DELETE SET NULL
|
||||||
|
FK : user_id -> user(id) ON DELETE SET NULL
|
||||||
|
IDX : (ingredient_id, created_at)
|
||||||
|
IDX : (movement_type, created_at)
|
||||||
|
```
|
||||||
|
|
||||||
|
| Column | Type | NULL | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
|
| `ingredient_id` | INT UNSIGNED | NO | FK -> ingredient |
|
||||||
|
| `movement_type` | ENUM('sale','cancellation','restock','inventory_correction') | NO | Nature of movement |
|
||||||
|
| `delta` | INT | NO | Signed change: negative for consumption, positive for restock/cancellation/correction |
|
||||||
|
| `order_id` | INT UNSIGNED | YES | FK -> customer_order; non-null for `sale` and `cancellation` |
|
||||||
|
| `user_id` | INT UNSIGNED | YES | FK -> user; null for automated sale decrements |
|
||||||
|
| `note` | VARCHAR(255) | YES | Optional human note |
|
||||||
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Immutable timestamp |
|
||||||
|
|
||||||
|
**ON DELETE RESTRICT** on `ingredient_id`: an ingredient with a movement history cannot be
|
||||||
|
deleted. Admin must archive the ingredient (`is_active = 0`) instead.
|
||||||
|
**ON DELETE SET NULL** on `order_id`: if an order is purged from the system, its movement
|
||||||
|
records remain with `order_id = NULL`. The audit log is preserved; only the order link is lost.
|
||||||
|
**ON DELETE SET NULL** on `user_id`: if a user is deleted, movement records remain with
|
||||||
|
`user_id = NULL`. Audit is preserved; individual attribution is lost.
|
||||||
|
|
||||||
|
**Immutability rule**: no UPDATE or DELETE at application layer. Corrections are new rows
|
||||||
|
with `movement_type = 'inventory_correction'` and a signed `delta`.
|
||||||
|
|
||||||
|
No `updated_at`. Immutable append-only table.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Referential integrity summary
|
||||||
|
|
||||||
|
| FK column | References | ON DELETE | Rationale |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `product.category_id` | `category(id)` | RESTRICT | No orphaned product |
|
||||||
|
| `menu.category_id` | `category(id)` | RESTRICT | Same |
|
||||||
|
| `menu.burger_product_id` | `product(id)` | RESTRICT | Menu definition requires its anchor burger |
|
||||||
|
| `menu_slot.menu_id` | `menu(id)` | CASCADE | Slots have no meaning without their menu |
|
||||||
|
| `menu_slot_option.menu_slot_id` | `menu_slot(id)` | CASCADE | Eligibility list disappears with the slot |
|
||||||
|
| `menu_slot_option.product_id` | `product(id)` | RESTRICT | Removing a product must not silently break menus |
|
||||||
|
| `product_ingredient.product_id` | `product(id)` | CASCADE | Recipe disappears with the product |
|
||||||
|
| `product_ingredient.ingredient_id` | `ingredient(id)` | RESTRICT | Cannot remove ingredient still in a recipe |
|
||||||
|
| `ingredient_allergen.ingredient_id` | `ingredient(id)` | CASCADE | Allergen links disappear with the ingredient |
|
||||||
|
| `ingredient_allergen.allergen_id` | `allergen(id)` | RESTRICT | Regulated allergen catalogue is immutable |
|
||||||
|
| `user.role_id` | `role(id)` | RESTRICT | A user cannot exist without a role |
|
||||||
|
| `role_visible_source.role_id` | `role(id)` | CASCADE | Dashboard filters disappear with the role |
|
||||||
|
| `role_permission.role_id` | `role(id)` | CASCADE | Permission mappings disappear with the role |
|
||||||
|
| `role_permission.permission_id` | `permission(id)` | CASCADE | Permission mappings disappear with the permission |
|
||||||
|
| `order_item.order_id` | `customer_order(id)` | CASCADE | Lines disappear with the order |
|
||||||
|
| `order_item.product_id` | `product(id)` | RESTRICT | Historical reference must not be silently orphaned |
|
||||||
|
| `order_item.menu_id` | `menu(id)` | RESTRICT | Same |
|
||||||
|
| `order_item_selection.order_item_id` | `order_item(id)` | CASCADE | Slot choices disappear with the line |
|
||||||
|
| `order_item_selection.menu_slot_id` | `menu_slot(id)` | RESTRICT | Historical slot record preserved |
|
||||||
|
| `order_item_selection.product_id` | `product(id)` | RESTRICT | Historical choice record preserved |
|
||||||
|
| `order_item_modifier.order_item_id` | `order_item(id)` | CASCADE | Modifiers disappear with the line |
|
||||||
|
| `order_item_modifier.ingredient_id` | `ingredient(id)` | RESTRICT | Historical modifier record preserved |
|
||||||
|
| `stock_movement.ingredient_id` | `ingredient(id)` | RESTRICT | Ingredient with history cannot be deleted |
|
||||||
|
| `stock_movement.order_id` | `customer_order(id)` | SET NULL | Audit preserved, order link lost |
|
||||||
|
| `stock_movement.user_id` | `user(id)` | SET NULL | Audit preserved, user attribution lost |
|
||||||
|
|
||||||
|
**Key used**: CASCADE = child has no meaning without parent; RESTRICT = parent deletion
|
||||||
|
blocked while children exist; SET NULL = child is preserved, only the link is severed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. CHECK constraints summary
|
||||||
|
|
||||||
|
| Table | CHECK expression | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `product` | `price_cents > 0` | Zero or negative price is a bug |
|
||||||
|
| `product` | `vat_rate IN (55, 100)` | Only two legal VAT rates for this model |
|
||||||
|
| `menu` | `price_normal_cents > 0` | Same as product |
|
||||||
|
| `menu` | `price_maxi_cents > 0` | Same |
|
||||||
|
| `ingredient` | `stock_quantity >= 0` | Negative stock is an alert, not a valid state |
|
||||||
|
| `ingredient` | `pack_size > 0` | Pack size of zero makes restock logic incoherent |
|
||||||
|
| `ingredient` | `low_stock_threshold >= 0` | Threshold cannot be negative |
|
||||||
|
| `product_ingredient` | `quantity_normal > 0` | Recipe quantity of zero is meaningless |
|
||||||
|
| `product_ingredient` | `quantity_maxi >= quantity_normal` | Maxi consumes at least as much as Normal (side/drink more, burger/sauce equal) |
|
||||||
|
| `product_ingredient` | `extra_price_cents >= 0` | No negative surcharge |
|
||||||
|
| `customer_order` | `total_ht_cents >= 0` | Zero is allowed (edge case during cart building) |
|
||||||
|
| `customer_order` | `total_vat_cents >= 0` | Same |
|
||||||
|
| `customer_order` | `total_ttc_cents > 0` | A validated order must have a positive total |
|
||||||
|
| `customer_order` | `total_ttc_cents = total_ht_cents + total_vat_cents` | Arithmetic invariant; defence-in-depth vs application bugs |
|
||||||
|
| `customer_order` | `source != 'drive' OR service_mode = 'drive'` | Cross-dimension constraint (dict. note 5) |
|
||||||
|
| `order_item` | `unit_price_cents_snapshot > 0` | Non-zero price at transaction time |
|
||||||
|
| `order_item` | `vat_rate_snapshot IN (55, 100)` | Snapshot must match allowed rates |
|
||||||
|
| `order_item` | `quantity > 0` | Non-zero quantity |
|
||||||
|
| `order_item` | `(item_type='product' AND product_id IS NOT NULL AND menu_id IS NULL) OR (item_type='menu' AND menu_id IS NOT NULL AND product_id IS NULL)` | Polymorphism: exactly one FK populated per discriminator value |
|
||||||
|
| `order_item_modifier` | `extra_price_cents >= 0` | Snapshot of surcharge; cannot be negative |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Recommended indexes (beyond PK / UK / FK auto-indexes)
|
||||||
|
|
||||||
|
MariaDB InnoDB creates an index automatically for each FK declaration (if no usable index
|
||||||
|
exists). The following additional indexes target frequent query patterns identified in the
|
||||||
|
MCT / MLT.
|
||||||
|
|
||||||
|
| Table | Index columns | Query pattern |
|
||||||
|
|---|---|---|
|
||||||
|
| `product` | `(category_id, is_available, display_order)` | Kiosk catalogue load: filter by category + availability, sort by order |
|
||||||
|
| `menu` | `(category_id, is_available, display_order)` | Same pattern for menus |
|
||||||
|
| `menu_slot` | `(menu_id, display_order)` | Menu builder: load all slots of a menu in order |
|
||||||
|
| `customer_order` | `(status, created_at)` | Active orders queue: pending/paid orders sorted by time |
|
||||||
|
| `customer_order` | `(source, created_at)` | Per-channel analytics and order filtering |
|
||||||
|
| `customer_order` | `created_at` | Time-range aggregations (hourly stats, `service_day`) |
|
||||||
|
| `order_item` | `order_id` | Retrieve all lines of an order |
|
||||||
|
| `order_item_selection` | `order_item_id` | Retrieve slot choices for a menu line |
|
||||||
|
| `order_item_modifier` | `order_item_id` | Retrieve ingredient modifications for a line |
|
||||||
|
| `stock_movement` | `(ingredient_id, created_at)` | Per-ingredient stock history (dict. section 3.19) |
|
||||||
|
| `stock_movement` | `(movement_type, created_at)` | Stats: cancellations per week, restocks per month |
|
||||||
|
| `role_permission` | `permission_id` | Reverse query: "which roles have this permission?" |
|
||||||
|
| `user` | `(is_active, role_id)` | Login check + permission resolution |
|
||||||
|
|
||||||
|
**Indexes not added** (intentional):
|
||||||
|
- `customer_order.order_number`: UK index is sufficient; no range query expected on this column.
|
||||||
|
- `customer_order.service_mode`: low cardinality (3 values); full scan on the status index
|
||||||
|
with a `service_mode` filter is acceptable at expected volume.
|
||||||
|
- `customer_order.paid_at`: NULL for most in-flight rows; sparse index provides limited benefit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Cross-validation MLD <-> MCD
|
||||||
|
|
||||||
|
Verification that all 19 MCD entities map to a table, and that all tables trace to the MCD.
|
||||||
|
|
||||||
|
| MCD entity | MLD table | Mapping type | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `category` (C1) | `category` (4.1) | 1:1 entity | |
|
||||||
|
| `product` (C2) | `product` (4.2) | 1:1 entity | |
|
||||||
|
| `menu` (C3) | `menu` (4.3) | 1:1 entity | New: `burger_product_id`, `price_normal_cents`, `price_maxi_cents` |
|
||||||
|
| `menu_slot` (C4) | `menu_slot` (4.4) | 1:1 entity | New entity (v0.2) |
|
||||||
|
| `menu_slot_option` (C5) | `menu_slot_option` (4.5) | Join table (composite PK) | New entity (v0.2) |
|
||||||
|
| `ingredient` (C6) | `ingredient` (4.6) | 1:1 entity | New entity (v0.2) |
|
||||||
|
| `product_ingredient` (C7) | `product_ingredient` (4.7) | Join table with attributes | New entity (v0.2) |
|
||||||
|
| `allergen` (C8) | `allergen` (4.8) | 1:1 entity | New entity (v0.2) |
|
||||||
|
| `ingredient_allergen` (C9) | `ingredient_allergen` (4.9) | Join table (composite PK) | New entity (v0.2) |
|
||||||
|
| `role` (C10) | `role` (4.10) | 1:1 entity | New: `default_route`, `order_source` |
|
||||||
|
| `user` (C11) | `user` (4.11) | 1:1 entity | Columns renamed to English |
|
||||||
|
| `role_visible_source` (C12) | `role_visible_source` (4.12) | Join table (composite PK) | New entity (v0.2) |
|
||||||
|
| `permission` (C13) | `permission` (4.13) | 1:1 entity | |
|
||||||
|
| `role_permission` (C14) | `role_permission` (4.14) | Join table (composite PK) | |
|
||||||
|
| `customer_order` (C15) | `customer_order` (4.15) | 1:1 entity | Renamed from `commande`; 4-state machine; phase timestamps |
|
||||||
|
| `order_item` (C16) | `order_item` (4.16) | 1:1 entity | New: `format`, `vat_rate_snapshot`; polymorphism CHECK |
|
||||||
|
| `order_item_selection` (C17) | `order_item_selection` (4.17) | 1:1 entity | New entity (v0.2) |
|
||||||
|
| `order_item_modifier` (C18) | `order_item_modifier` (4.18) | 1:1 entity | New entity (v0.2) |
|
||||||
|
| `stock_movement` (C19) | `stock_movement` (4.19) | 1:1 entity | New entity (v0.2) |
|
||||||
|
|
||||||
|
**Result**: 19/19 entities mapped. No entity without a table; no table outside the MCD.
|
||||||
|
|
||||||
|
**Dropped from v0.1**: `commande_event` (replaced by `paid_at`, `delivered_at`, `cancelled_at`
|
||||||
|
phase timestamps on `customer_order` — decision 2.A); `menu_produit` fixed-composition model
|
||||||
|
(replaced by `menu_slot` + `menu_slot_option` — decision D1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Volume estimation (6 months)
|
||||||
|
|
||||||
|
| Table | Rows at 6 months | Avg row size | Est. size |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `category` | ~10 | 200 bytes | < 1 KB |
|
||||||
|
| `product` | ~55 | 400 bytes | ~22 KB |
|
||||||
|
| `menu` | ~13 | 450 bytes | ~6 KB |
|
||||||
|
| `menu_slot` | ~40 | 150 bytes | ~6 KB |
|
||||||
|
| `menu_slot_option` | ~150 | 30 bytes | ~5 KB |
|
||||||
|
| `ingredient` | ~100 | 300 bytes | ~30 KB |
|
||||||
|
| `product_ingredient` | ~400 | 40 bytes | ~16 KB |
|
||||||
|
| `allergen` | 14 | 200 bytes | ~3 KB |
|
||||||
|
| `ingredient_allergen` | ~200 | 20 bytes | ~4 KB |
|
||||||
|
| `role` | ~5 | 200 bytes | ~1 KB |
|
||||||
|
| `user` | ~20 | 500 bytes | ~10 KB |
|
||||||
|
| `role_visible_source` | ~7 | 15 bytes | < 1 KB |
|
||||||
|
| `permission` | 23 | 250 bytes | ~6 KB |
|
||||||
|
| `role_permission` | ~80 | 15 bytes | ~2 KB |
|
||||||
|
| `customer_order` | ~30k | 300 bytes | ~9 MB |
|
||||||
|
| `order_item` | ~150k | 250 bytes | ~37 MB |
|
||||||
|
| `order_item_selection` | ~300k | 150 bytes | ~45 MB |
|
||||||
|
| `order_item_modifier` | ~150k | 80 bytes | ~12 MB |
|
||||||
|
| `stock_movement` | ~500k | 180 bytes | ~90 MB |
|
||||||
|
|
||||||
|
**Estimated total**: ~190 MB data + ~60-80 MB for indexes = ~250-270 MB over 6 months.
|
||||||
|
Manageable on the MariaDB container (`wakdo_db_data` named volume in `docker-compose.yml`).
|
||||||
|
|
||||||
|
`stock_movement` is the highest-volume table (~5-15 rows per order across all ingredients).
|
||||||
|
The `(ingredient_id, created_at)` index is the primary query path for per-ingredient
|
||||||
|
history; it will carry meaningful write amplification at scale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Decisions deferred to DDL and P2
|
||||||
|
|
||||||
|
1. **MariaDB generated column** for `service_day`: a `VIRTUAL GENERATED` column is technically
|
||||||
|
possible in MariaDB 5.7+ syntax. If stats queries prove burdensome without a materialised
|
||||||
|
column, a `STORED GENERATED` column could be added as a migration. For this model, the
|
||||||
|
applicative CASE expression is retained (simpler, avoids generated-column edge cases).
|
||||||
|
2. **Partitioning**: `stock_movement` could be partitioned by month if volume exceeds
|
||||||
|
estimates. Not in scope for the initial DDL.
|
||||||
|
3. **Triggers**: stock decrement on `paid` transition and re-credit on `cancelled` (from `paid`)
|
||||||
|
could be implemented as MariaDB triggers or as application-layer logic. To be decided at P2.
|
||||||
|
4. **Collation**: `utf8mb4_unicode_ci` retained (Unicode-compliant, case-insensitive).
|
||||||
|
If strict French alphabetical sort is needed, `utf8mb4_fr_0900_ai_ci` is available in
|
||||||
|
MySQL 8 but not MariaDB; `unicode_ci` is the portable choice.
|
||||||
|
5. **Migration tooling**: Phinx, Doctrine Migrations, or a plain PHP script. Decision at P2.
|
||||||
|
6. **`order_item_id` constraint for selections**: the business rule that
|
||||||
|
`order_item_selection.order_item_id` must reference a line with `item_type='menu'`
|
||||||
|
is enforced at application layer. A MariaDB trigger could reinforce this at DB level if
|
||||||
|
needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Next steps (DDL + Seed)
|
||||||
|
|
||||||
|
1. **DDL** (`db/migrations/0001_init_schema.sql`): transcribe this MLD into executable
|
||||||
|
`CREATE TABLE` statements, in dependency order:
|
||||||
|
- `category` -> `product`, `ingredient`, `allergen`, `role`
|
||||||
|
- `menu` (depends on `category`, `product`)
|
||||||
|
- `menu_slot` (depends on `menu`), `menu_slot_option` (depends on `menu_slot`, `product`)
|
||||||
|
- `product_ingredient` (depends on `product`, `ingredient`)
|
||||||
|
- `ingredient_allergen` (depends on `ingredient`, `allergen`)
|
||||||
|
- `user` (depends on `role`), `role_visible_source` (depends on `role`)
|
||||||
|
- `permission`, `role_permission` (depends on `role`, `permission`)
|
||||||
|
- `customer_order`
|
||||||
|
- `order_item` (depends on `customer_order`, `product`, `menu`)
|
||||||
|
- `order_item_selection` (depends on `order_item`, `menu_slot`, `product`)
|
||||||
|
- `order_item_modifier` (depends on `order_item`, `ingredient`)
|
||||||
|
- `stock_movement` (depends on `ingredient`, `customer_order`, `user`)
|
||||||
|
|
||||||
|
2. **Seed** (`db/seeds/0001_demo_data.sql`):
|
||||||
|
- 9 categories + 53 products + 13 menus from JSON sources (`docs/merise/_sources/`)
|
||||||
|
- 13 menus with slots and slot options
|
||||||
|
- 14 allergens (INCO EU 1169/2011)
|
||||||
|
- Sample ingredient catalogue with recipes
|
||||||
|
- 5 roles with `role_permission` matrix and `role_visible_source` data
|
||||||
|
- 1 admin user
|
||||||
|
- Sample orders for demo
|
||||||
|
|
||||||
|
3. **Fallback JSON export** (`scripts/export-fallback.{sh|php}`): extract seed data to
|
||||||
|
`src/public/borne/data/*.json` for isolated kiosk mode (Bloc 1 without DB).
|
||||||
|
|
||||||
|
4. **DDL validation tests**: confirm CHECK constraints trigger as expected; confirm
|
||||||
|
ON DELETE CASCADE / RESTRICT / SET NULL behaviours match specification.
|
||||||
637
docs/merise/mlt.md
Normal file
637
docs/merise/mlt.md
Normal file
|
|
@ -0,0 +1,637 @@
|
||||||
|
# Model of Logical Treatments (MLT) — Wakdo
|
||||||
|
|
||||||
|
**Merise phase** : P1 - Conception, step 4 (derived from MCT)
|
||||||
|
**Version** : v0.2 — prod-like, 4-state machine
|
||||||
|
**Date** : 2026-06-04
|
||||||
|
**Branch** : `feat/p1-conception`
|
||||||
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
The MLT (Model of Logical Treatments) refines each MCT operation by specifying:
|
||||||
|
- **preconditions** — what must be true before execution
|
||||||
|
- **business rules** — validation, computation, business logic
|
||||||
|
- **postconditions** — the state guaranteed after success
|
||||||
|
- **outputs** — produced data or emitted events
|
||||||
|
- **error cases** — alternative outputs when a condition fails
|
||||||
|
|
||||||
|
It bridges the MCT (conceptual level) and the PHP/SQL implementation (physical level).
|
||||||
|
All entity/attribute references use the names from `docs/merise/dictionary.md` (English,
|
||||||
|
snake_case). All monetary amounts are in integer cents.
|
||||||
|
|
||||||
|
**Tag conventions**:
|
||||||
|
- `[PRE]` — precondition; must be satisfied for the operation to execute
|
||||||
|
- `[RG]` — business rule (regle de gestion); logic applied during execution
|
||||||
|
- `[POST]` — postcondition; database state guaranteed after success
|
||||||
|
- `[OUT]` — output; data or event produced
|
||||||
|
- `[ERR]` — error case; alternative output when a condition fails
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Transverse business rules
|
||||||
|
|
||||||
|
These rules apply to multiple operations and are centralised here to avoid repetition.
|
||||||
|
|
||||||
|
| Rule code | Label | Operations concerned |
|
||||||
|
|-----------|-------|----------------------|
|
||||||
|
| **RG-T01** | CSRF token verified on every back-office POST/PUT/DELETE form | AUTH, all admin ops |
|
||||||
|
| **RG-T02** | Session active + `user.is_active = 1` verified on each authenticated request | All domains 3-10 |
|
||||||
|
| **RG-T03** | Permission verified via `role_permission` before executing operation | All domains 3-10 |
|
||||||
|
| **RG-T04** | All monetary amounts are manipulated in integer cents; EUR conversion at output only | 3.3, 4.1, 8.1, 8.4 |
|
||||||
|
| **RG-T05** | Snapshots (`label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot`) on `order_item` are not modified after INSERT (historical integrity of placed orders — design guarantee) | 3.3, 4.1, 8.2, 8.5 |
|
||||||
|
| **RG-T06** | All SQL queries use PDO with prepared statements; no user data concatenated into SQL | All operations |
|
||||||
|
| **RG-T07** | Status transition UPDATE statements include `AND status = <expected_status>` in the WHERE clause (optimistic concurrency protection against double transition) | 6.1, 7.1 |
|
||||||
|
| **RG-T08** | Operations touching multiple tables execute in an atomic database transaction; partial failure triggers full rollback | 3.3, 4.1, 7.1, 8.4, 9.1, 9.2 |
|
||||||
|
| **RG-T09** | Cross-constraint on `customer_order`: `source = 'drive'` implies `service_mode = 'drive'`; verified at order creation. Materialisable as a MariaDB CHECK: `CHECK (source != 'drive' OR service_mode = 'drive')`. | 3.3, 4.1 |
|
||||||
|
| **RG-T10** | VAT computation is line-by-line: each `order_item` carries its own `vat_rate_snapshot` (per-mille integer snapshotted from `product.vat_rate`). Order totals (`total_ht_cents`, `total_vat_cents`, `total_ttc_cents`) are the sum of line-level amounts. | 3.3, 4.1 |
|
||||||
|
| **RG-T11** | Stock decrements at the `pending_payment -> paid` transition and re-credits at `paid -> cancelled` are within the same database transaction as the status update (no orphan decrement). | 3.3, 4.1, 7.1 |
|
||||||
|
| **RG-T12** | Dashboard filter by source: each role's visible sources are read from `role_visible_source`; the query uses `WHERE customer_order.source IN (role_visible_sources)`. | 6.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Domain 1 — Order lifecycle (kiosk)
|
||||||
|
|
||||||
|
### 3.1 LOAD_CATALOGUE
|
||||||
|
|
||||||
|
**Corresponds to MCT section 3.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Request originates from the kiosk endpoint (public, no authentication required) |
|
||||||
|
| **[PRE-2]** | Current time is within the service window (10:00-01:00); outside the window the kiosk displays a closed message |
|
||||||
|
| **[RG-1]** | Read all `category` rows with `is_active = 1`, ordered by `category.display_order ASC` |
|
||||||
|
| **[RG-2]** | For each category, read `product` rows with `is_available = 1` and matching `category_id`, ordered by `product.display_order ASC` |
|
||||||
|
| **[RG-3]** | Read all `menu` rows with `is_available = 1`; for each menu, load `menu_slot` rows ordered by `menu_slot.display_order ASC`; for each slot, load eligible products via `menu_slot_option JOIN product` (where `product.is_available = 1`) |
|
||||||
|
| **[RG-4]** | For each product, compute allergens by joining `product_ingredient -> ingredient_allergen -> allergen` (no manual re-entry per product) |
|
||||||
|
| **[RG-5]** | For each product with `product_ingredient` rows, load `ingredient` composition (for the configurator) |
|
||||||
|
| **[RG-6]** | Prices are returned in integer cents; EUR conversion is performed client-side |
|
||||||
|
| **[POST-1]** | No database write; database state unchanged |
|
||||||
|
| **[OUT-1]** | JSON response: `{data: {categories: [...], products: {...}, menus: [{..., slots: [{..., options: [...]}]}]}}` |
|
||||||
|
| **[ERR-1]** | DB unreachable: response `{data: null, error: {code: "DB_ERROR"}}` and front-end falls back to static JSON |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 COMPOSE_CART
|
||||||
|
|
||||||
|
**Corresponds to MCT section 3.2**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Catalogue loaded into front-end memory (LOAD_CATALOGUE completed) |
|
||||||
|
| **[PRE-2]** | Selected item (product or menu) is present in the loaded catalogue with `is_available = 1` |
|
||||||
|
| **[RG-1]** | Cart is a JavaScript in-memory structure (array of items); no database persistence at this stage |
|
||||||
|
| **[RG-2]** | Each item contains: `type` (`product` or `menu`), `item_id`, `label`, `unit_price_cents` (snapshot from catalogue), `quantity`, `format` (`normal` or `maxi`, for menus), `slot_selections` (array of `{menu_slot_id, product_id, label}` for menu items), `modifiers` (array of `{ingredient_id, action, extra_price_cents}`) |
|
||||||
|
| **[RG-3]** | Format Normal/Maxi (menu items only): `normal` uses `menu.price_normal_cents`; `maxi` uses `menu.price_maxi_cents`. No individual component price change is stored; the price differential is at menu level. |
|
||||||
|
| **[RG-4]** | Ingredient modifier rules: `action = 'remove'` requires `is_removable = 1` on `product_ingredient` (free); `action = 'add'` requires `is_addable = 1` (may carry `extra_price_cents`). These constraints are verified at cart composition time against the loaded catalogue. |
|
||||||
|
| **[RG-5]** | If an item with the same `(type, item_id, format, slot_selections, modifiers)` already exists in the cart, its quantity is incremented rather than adding a new item |
|
||||||
|
| **[RG-6]** | Cart total recomputed after each change: `SUM(unit_price_cents * quantity + modifier_extras)` across all items |
|
||||||
|
| **[POST-1]** | No database write; cart in-memory state updated |
|
||||||
|
| **[OUT-1]** | Cart summary displayed with TTC total |
|
||||||
|
| **[ERR-1]** | If a product becomes `is_available = 0` between catalogue load and order submission, the server-side validation in CREATE_ORDER catches it |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 CREATE_ORDER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 3.3**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Cart contains at least 1 item (`items.length >= 1`) |
|
||||||
|
| **[PRE-2]** | Order number entered by customer is non-empty (front-end validation) |
|
||||||
|
| **[PRE-3]** | POST JSON body is valid (schema validation at API layer) |
|
||||||
|
| **[RG-1]** | Server-side availability check: for each item, verify `product.is_available = 1` or `menu.is_available = 1`. If any item is unavailable, reject with list of unavailable articles. |
|
||||||
|
| **[RG-2 — service_day]** | `service_day` for a given order is computed at query time as: `CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END`. Cutoff is 10:00. This is NOT stored as a column — computed at query time only. The v0.1 formula with `INTERVAL 4 HOUR 30 MINUTE` was incorrect and is dropped. |
|
||||||
|
| **[RG-3 — order number]** | Order number format: `K-YYYY-MM-DD-NNN` where NNN is the sequential counter for the current service_day for the `kiosk` source (SELECT COUNT + 1 with a table-level lock or serialised insert to avoid duplicate generation under concurrency). Source is `kiosk` (set by the kiosk endpoint, derived from the public entry point). |
|
||||||
|
| **[RG-4 — VAT by line]** | For each `order_item`: `vat_rate_snapshot` is copied from `product.vat_rate`. Line amounts: `unit_ttc = unit_price_cents_snapshot`; `unit_ht = ROUND(unit_ttc * 1000 / (1000 + vat_rate_snapshot))`; `unit_vat = unit_ttc - unit_ht`. Order totals: `total_ttc_cents = SUM(unit_ttc * quantity)` across all lines; `total_ht_cents = SUM(unit_ht * quantity)`; `total_vat_cents = total_ttc_cents - total_ht_cents`. Invariant: `total_ttc_cents = total_ht_cents + total_vat_cents` (verified before INSERT). |
|
||||||
|
| **[RG-5 — atomic transaction]** | All writes within one database transaction: (1) INSERT `customer_order` (status `pending_payment`, source `kiosk`, service_mode from cart, computed totals); (2) INSERT `order_item` rows (label_snapshot, unit_price_cents_snapshot, vat_rate_snapshot, quantity, format, item_type, product_id or menu_id); (3) INSERT `order_item_selection` rows for each slot filled in a menu item (order_item_id, menu_slot_id, product_id, label_snapshot); (4) INSERT `order_item_modifier` rows for each ingredient modification (order_item_id, ingredient_id, action, extra_price_cents snapshot); (5) for each ingredient consumed: compute units = `(order_item.format = 'maxi' ? product_ingredient.quantity_maxi : product_ingredient.quantity_normal) * order_item.quantity`, adjusted by modifiers (remove => no decrement for that ingredient; add => extra decrement); UPDATE `ingredient.stock_quantity -= units`; INSERT `stock_movement` (type `sale`, delta = -units, order_id, user_id = NULL for kiosk); (6) UPDATE `customer_order` SET status = `paid`, `paid_at = NOW()`. All six steps commit together or roll back entirely. |
|
||||||
|
| **[RG-6 — cross-constraint]** | Source `kiosk` implies no particular service_mode constraint; the customer selects `dine_in` or `takeaway`. The drive cross-constraint (RG-T09) does not apply to kiosk-originated orders. |
|
||||||
|
| **[RG-7 — immutability]** | After INSERT, `label_snapshot`, `unit_price_cents_snapshot`, and `vat_rate_snapshot` are not modified even if the source product is later renamed or repriced (see RG-T05). |
|
||||||
|
| **[POST-1]** | One `customer_order` row exists with `status = 'paid'`, `source = 'kiosk'`, all totals computed, `paid_at` set. The `pending_payment` phase is not observable outside the transaction. |
|
||||||
|
| **[POST-2]** | N `order_item` rows exist, each referencing either a `product_id` (item_type='product') or a `menu_id` (item_type='menu') — exclusivity constraint verified. |
|
||||||
|
| **[POST-3]** | `customer_order.order_number` is unique in the database (UNIQUE constraint). |
|
||||||
|
| **[POST-4]** | `ingredient.stock_quantity` decremented for each consumed ingredient unit; one `stock_movement` row of type `sale` per affected ingredient. |
|
||||||
|
| **[OUT-1]** | HTTP 201: `{data: {id: int, order_number: string, status: 'paid'}}` |
|
||||||
|
| **[OUT-2]** | Logical event ORDER_CREATED available for preparation domain (preparation display refreshes via polling or server push depending on implementation) |
|
||||||
|
| **[ERR-1]** | Empty cart: HTTP 422, `{error: {code: "EMPTY_CART"}}` |
|
||||||
|
| **[ERR-2]** | Unavailable item: HTTP 422, `{error: {code: "ITEM_UNAVAILABLE", items: [...]}}` |
|
||||||
|
| **[ERR-3]** | DB error / timeout: HTTP 500 with rollback, `{error: {code: "DB_ERROR"}}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 DISPLAY_CONFIRMATION
|
||||||
|
|
||||||
|
**Corresponds to MCT section 3.4**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | CREATE_ORDER returned HTTP 201 with `{id, order_number, status: 'paid'}` |
|
||||||
|
| **[RG-1]** | Order number displayed prominently on the confirmation screen |
|
||||||
|
| **[RG-2]** | After a configurable delay (suggestion: 15 seconds), the kiosk auto-resets for the next customer |
|
||||||
|
| **[POST-1]** | No database write |
|
||||||
|
| **[OUT-1]** | Confirmation screen displayed with order number |
|
||||||
|
| **[ERR-1]** | If API response is an error: generic error message displayed with option to retry |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Domain 2 — Order lifecycle (counter and drive)
|
||||||
|
|
||||||
|
### 4.1 CREATE_COUNTER_ORDER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 4.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor is authenticated (valid session, `user.is_active = 1`) |
|
||||||
|
| **[PRE-2]** | Actor holds permission `order.create` (verified via `role_permission`) |
|
||||||
|
| **[PRE-3]** | Cart contains at least 1 item |
|
||||||
|
| **[RG-1]** | Creation logic identical to CREATE_ORDER (RG-1 through RG-7 apply), with the following differences: `source` is auto-tagged from `role.order_source` (counter role -> `counter`, drive role -> `drive`); `service_mode` is selected by the staff member (`dine_in` / `takeaway` / `drive`); `user_id` is set to the authenticated user's id in `stock_movement` rows (instead of NULL for kiosk). |
|
||||||
|
| **[RG-2 — cross-constraint]** | If `source = 'drive'` then `service_mode` must be `'drive'` (RG-T09); verified before INSERT. HTTP 422 if violated. |
|
||||||
|
| **[RG-3 — order number]** | Format: `C-YYYY-MM-DD-NNN` for counter source; `D-YYYY-MM-DD-NNN` for drive source. Sequential NNN counter is per source per service_day. |
|
||||||
|
| **[RG-4 — stock]** | Same stock decrement logic as CREATE_ORDER RG-5; `stock_movement.user_id` is set to the authenticated staff member's id. |
|
||||||
|
| **[POST-1]** | One `customer_order` row with `status = 'paid'`, `source = 'counter'` or `'drive'`, `paid_at` set. |
|
||||||
|
| **[POST-2]** | N `order_item` rows with snapshots. Slot selections and modifiers written identically to kiosk flow. |
|
||||||
|
| **[POST-3]** | Stock decremented; movements logged with actor `user_id`. |
|
||||||
|
| **[OUT-1]** | HTTP 201: `{data: {id: int, order_number: string, status: 'paid'}}`. Order number communicated to customer. |
|
||||||
|
| **[ERR-1]** | Same error cases as CREATE_ORDER (ERR-1, ERR-2, ERR-3) |
|
||||||
|
| **[ERR-2]** | Cross-constraint violation (`source = drive` but `service_mode != drive`): HTTP 422, `{error: {code: "INVALID_SERVICE_MODE"}}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Domain 3 — Preparation display (kitchen)
|
||||||
|
|
||||||
|
### 5.1 LIST_ORDERS_DISPLAY
|
||||||
|
|
||||||
|
**Corresponds to MCT section 5.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor is authenticated, `is_active = 1` |
|
||||||
|
| **[PRE-2]** | Actor holds permission `order.read` |
|
||||||
|
| **[RG-1 — source filter]** | Retrieve visible sources for the actor's role: `SELECT source FROM role_visible_source WHERE role_id = :role_id`. Kitchen sees all three; counter sees `kiosk` and `counter`; drive sees `drive`. |
|
||||||
|
| **[RG-2 — query]** | `SELECT customer_order.*, order_item.* FROM customer_order JOIN order_item ON order_item.order_id = customer_order.id WHERE customer_order.status = 'paid' AND customer_order.source IN (:visible_sources) ORDER BY customer_order.paid_at ASC` |
|
||||||
|
| **[RG-3 — item detail]** | For each order line of type `menu`, also load `order_item_selection` rows (slot choices). For all lines, load `order_item_modifier` rows (ingredient modifications). Display uses snapshots (`label_snapshot`, `quantity`, `format`); no re-join on `product` or `menu` tables needed. |
|
||||||
|
| **[RG-4 — KDS colour]** | Colour indicator computed at render time: `elapsed = NOW() - customer_order.paid_at`; green if elapsed < SLA threshold (configurable, approx. 10 min); amber if approaching; red if exceeded. Not stored; computed client-side or in PHP before response. |
|
||||||
|
| **[RG-5 — read only]** | Kitchen staff perform no status transition from this view. No UPDATE is issued by this operation. |
|
||||||
|
| **[POST-1]** | No database write |
|
||||||
|
| **[OUT-1]** | List of orders with status `paid`, filtered by role, sorted by `paid_at` ascending, with full item detail (selections, modifiers, KDS colour) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Domain 4 — Delivery to customer
|
||||||
|
|
||||||
|
### 6.1 DELIVER_ORDER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 6.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor is authenticated, holds permission `order.deliver` |
|
||||||
|
| **[PRE-2]** | Targeted order exists and `status = 'paid'` |
|
||||||
|
| **[PRE-3]** | Order source is in the actor's visible sources (verified via `role_visible_source`) |
|
||||||
|
| **[RG-1]** | `UPDATE customer_order SET status = 'delivered', delivered_at = NOW(), updated_at = NOW() WHERE id = :id AND status = 'paid'` |
|
||||||
|
| **[RG-2 — concurrency]** | The `AND status = 'paid'` clause in the UPDATE protects against concurrent double-delivery: if two staff members click simultaneously, only the first succeeds (second receives 0 rows affected). |
|
||||||
|
| **[RG-3]** | `delivered` is a terminal status: no further transition is defined from this status (application constraint, not enforced as a DB trigger). |
|
||||||
|
| **[POST-1]** | `customer_order.status = 'delivered'`, `delivered_at` set, lifecycle complete. Order passes to history. |
|
||||||
|
| **[OUT-1]** | HTTP 200 with confirmation. Order disappears from the `paid` queue. |
|
||||||
|
| **[ERR-1]** | Invalid transition (status was not `paid` when UPDATE executed — concurrency): HTTP 409, `{error: {code: "INVALID_TRANSITION"}}` |
|
||||||
|
| **[ERR-2]** | Order source not in actor's visible sources: HTTP 403, `{error: {code: "FORBIDDEN"}}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Domain 5 — Cancellation
|
||||||
|
|
||||||
|
### 7.1 CANCEL_ORDER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 7.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor is authenticated, holds permission `order.cancel` |
|
||||||
|
| **[PRE-2]** | Targeted order exists |
|
||||||
|
| **[PRE-3]** | `customer_order.status` is in `['pending_payment', 'paid']`. Terminal statuses `delivered` and `cancelled` cannot transition to `cancelled`. |
|
||||||
|
| **[RG-1 — status update]** | `UPDATE customer_order SET status = 'cancelled', cancelled_at = NOW(), updated_at = NOW() WHERE id = :id AND status IN ('pending_payment', 'paid')` |
|
||||||
|
| **[RG-2 — concurrency]** | The `AND status IN (...)` clause protects against concurrent cancellation (see RG-T07). |
|
||||||
|
| **[RG-3 — stock re-credit — conditional]** | Re-credit applies only if the order was at status `paid` before cancellation. Orders at `pending_payment` had not yet decremented stock (the decrement occurs at the `paid` transition). For each `order_item` line of a `paid` order, recompute ingredient units consumed: `(order_item.format = 'maxi' ? product_ingredient.quantity_maxi : product_ingredient.quantity_normal) * order_item.quantity`, adjusted by `order_item_modifier` rows (remove modifier -> ingredient was not decremented, so no re-credit; add modifier -> ingredient had extra decrement, so extra re-credit). UPDATE `ingredient.stock_quantity += units`. INSERT `stock_movement` (type `cancellation`, delta = +units, order_id, user_id of actor). |
|
||||||
|
| **[RG-4 — transaction]** | Status update and stock re-credit (when applicable) execute in the same database transaction (RG-T11). |
|
||||||
|
| **[RG-5 — history]** | Order is not physically deleted; retained for history and stats. Cancelled orders are excluded from revenue totals but included in volume counts in READ_STATS. `order_item` rows are not deleted (ON DELETE CASCADE is not triggered); they allow reconstruction of what was ordered. |
|
||||||
|
| **[POST-1]** | `customer_order.status = 'cancelled'`, `cancelled_at` set, terminal state. |
|
||||||
|
| **[POST-2]** | If prior status was `paid`: `ingredient.stock_quantity` re-credited; one `stock_movement` row of type `cancellation` per affected ingredient. |
|
||||||
|
| **[OUT-1]** | HTTP 200 with cancellation confirmation |
|
||||||
|
| **[ERR-1]** | Attempt to cancel a delivered or already cancelled order: HTTP 422, `{error: {code: "CANNOT_CANCEL_IN_STATE", current_status: "..."}}` |
|
||||||
|
| **[ERR-2]** | Concurrent cancellation (0 rows affected by UPDATE): HTTP 409, `{error: {code: "INVALID_TRANSITION"}}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Domain 6 — Catalogue management
|
||||||
|
|
||||||
|
### 8.1 CREATE_PRODUCT
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `product.create` |
|
||||||
|
| **[PRE-2]** | `category_id` references an existing category with `is_active = 1` |
|
||||||
|
| **[RG-1]** | Form validation: `name` non-empty, `price_cents > 0`, `category_id` valid, `vat_rate` in `(55, 100)` |
|
||||||
|
| **[RG-2]** | Image upload (optional): validate MIME type (JPEG, PNG, WEBP), max size configurable (suggestion: 2 MB), store under `UPLOAD_DIR/products/`, record relative path in `image_path` |
|
||||||
|
| **[RG-3]** | `is_available = 1` by default at INSERT |
|
||||||
|
| **[RG-4]** | `display_order` set to `MAX(display_order) + 1` for the target category, or 0 if first product |
|
||||||
|
| **[POST-1]** | One `product` row in the database with all valid fields |
|
||||||
|
| **[OUT-1]** | Redirect to category product list with success message |
|
||||||
|
| **[ERR-1]** | Validation failure: inline field errors displayed |
|
||||||
|
| **[ERR-2]** | Invalid image (type or size): specific error message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.2 UPDATE_PRODUCT
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.2**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `product.update` |
|
||||||
|
| **[PRE-2]** | Target `product.id` exists |
|
||||||
|
| **[RG-1]** | Same validations as CREATE_PRODUCT on modified fields |
|
||||||
|
| **[RG-2]** | If a new image is uploaded, the old image file is deleted from the filesystem (volume cleanup) |
|
||||||
|
| **[RG-3]** | `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot` in historical `order_item` rows are not modified (see RG-T05) |
|
||||||
|
| **[POST-1]** | `product` updated, `updated_at` refreshed |
|
||||||
|
| **[OUT-1]** | Redirect to product list with success message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.3 DELETE_PRODUCT
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.3**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `product.delete` |
|
||||||
|
| **[PRE-2]** | Target `product.id` exists |
|
||||||
|
| **[RG-1]** | Pre-check (PHP): is the product referenced in `menu_slot_option.product_id`? If yes, display blocking message listing the menus. |
|
||||||
|
| **[RG-2]** | Pre-check (PHP): is the product the `burger_product_id` of any `menu`? If yes, block with message to delete or reassign the menu first. |
|
||||||
|
| **[RG-3]** | Pre-check (PHP): is the product referenced in `order_item.product_id` (historical orders)? FK `ON DELETE RESTRICT` blocks at DB level. Recommended response: propose deactivation (`is_available=0`) rather than deletion. |
|
||||||
|
| **[RG-4]** | FK constraints (`menu_slot_option.product_id ON DELETE RESTRICT`, `order_item.product_id ON DELETE RESTRICT`) enforce the constraint even if the PHP check is bypassed. |
|
||||||
|
| **[POST-1]** | Product deleted if no FK constraint was blocking |
|
||||||
|
| **[OUT-1]** | Redirect to product list with success message |
|
||||||
|
| **[ERR-1]** | Product in menu slot: HTTP 422 or inline message with blocking menu list |
|
||||||
|
| **[ERR-2]** | Product in historical orders: message proposing deactivation instead |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.4 CREATE_MENU
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.4**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `menu.create` |
|
||||||
|
| **[PRE-2]** | `burger_product_id` references an existing, available product |
|
||||||
|
| **[PRE-3]** | At least one `menu_slot` is defined with at least one `menu_slot_option` |
|
||||||
|
| **[RG-1]** | Validation: `name` non-empty, `price_normal_cents > 0`, `price_maxi_cents > 0`, `burger_product_id` valid, all `product_id` values in slot options exist |
|
||||||
|
| **[RG-2]** | Transaction: INSERT `menu`, then INSERT `menu_slot` rows (name, slot_type, is_required, display_order), then INSERT `menu_slot_option` rows (menu_slot_id, product_id) |
|
||||||
|
| **[RG-3]** | Valid `slot_type` values (from dictionary ENUM): `drink`, `side`, `sauce`, `dessert`, `extra` |
|
||||||
|
| **[POST-1]** | One `menu` row, N `menu_slot` rows, M `menu_slot_option` rows in the database |
|
||||||
|
| **[OUT-1]** | Redirect to menu list with success message |
|
||||||
|
| **[ERR-1]** | Invalid configuration (no slot, no option): business error message |
|
||||||
|
| **[ERR-2]** | Slot option product unavailable: warning (menu can be created; product availability is checked at order time) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.5 UPDATE_MENU
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.5**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `menu.update` |
|
||||||
|
| **[PRE-2]** | Target `menu.id` exists |
|
||||||
|
| **[RG-1]** | Same validations as CREATE_MENU on modified fields |
|
||||||
|
| **[RG-2]** | If slot configuration is modified: `DELETE FROM menu_slot_option WHERE menu_slot_id IN (SELECT id FROM menu_slot WHERE menu_id = :id)`, then `DELETE FROM menu_slot WHERE menu_id = :id`, then re-INSERT (delete-and-reinsert pattern, atomic in transaction) |
|
||||||
|
| **[RG-3]** | `label_snapshot` values in historical `order_item_selection` rows are not affected (see RG-T05) |
|
||||||
|
| **[POST-1]** | `menu` updated; `menu_slot` and `menu_slot_option` rebuilt |
|
||||||
|
| **[OUT-1]** | Redirect with success message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.6 DELETE_MENU
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.6**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `menu.delete` |
|
||||||
|
| **[PRE-2]** | Target `menu.id` exists |
|
||||||
|
| **[RG-1]** | Pre-check (PHP): is the menu referenced in `order_item.menu_id`? FK `ON DELETE RESTRICT`. If yes, propose deactivation (`is_available=0`) instead of deletion. |
|
||||||
|
| **[RG-2]** | If no historical reference: DELETE `menu` triggers CASCADE to `menu_slot` (which cascades to `menu_slot_option`) |
|
||||||
|
| **[POST-1]** | `menu`, its `menu_slot` rows, and its `menu_slot_option` rows deleted |
|
||||||
|
| **[OUT-1]** | Redirect with success message |
|
||||||
|
| **[ERR-1]** | Menu in historical orders: message proposing deactivation instead |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.7 MANAGE_CATEGORY
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.7**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `category.manage` |
|
||||||
|
| **[RG-CREATE]** | `name` and `slug` non-empty and unique in the database; `display_order` set to MAX + 1 |
|
||||||
|
| **[RG-UPDATE]** | UPDATE `name`, `slug`, `image_path`, `display_order`, `is_active` |
|
||||||
|
| **[RG-DEACTIVATE]** | Deactivation (`is_active=0`) does not auto-deactivate child products/menus in the DB (no CASCADE on `is_active`). PHP layer proposes to the admin to also deactivate child products/menus, or the kiosk filter on `category.is_active = 1` implicitly hides them. |
|
||||||
|
| **[RG-DELETE]** | Physical deletion blocked if `product.category_id` or `menu.category_id` references this category (FK `ON DELETE RESTRICT`). Propose deactivation. |
|
||||||
|
| **[POST-CREATE]** | New `category` row in database |
|
||||||
|
| **[POST-UPDATE]** | `category` updated, `updated_at` refreshed |
|
||||||
|
| **[OUT-1]** | Confirmation, redirect to category list |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8.8 MANAGE_INGREDIENT
|
||||||
|
|
||||||
|
**Corresponds to MCT section 8.8**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `ingredient.manage` |
|
||||||
|
| **[RG-CREATE-ING]** | `name` non-empty and UNIQUE; `unit` non-empty; `pack_size >= 1`; `low_stock_threshold >= 0`; `stock_quantity` defaults to 0 at creation |
|
||||||
|
| **[RG-UPDATE-ING]** | UPDATE `name`, `unit`, `pack_size`, `pack_label`, `low_stock_threshold`, `is_active` |
|
||||||
|
| **[RG-DEACTIVATE-ING]** | `is_active=0` hides ingredient from configurator. Physical deletion blocked if referenced in `product_ingredient` (FK `ON DELETE RESTRICT`) or `stock_movement` (FK `ON DELETE RESTRICT`). |
|
||||||
|
| **[RG-COMPOSITION]** | UPDATE `product_ingredient`: for each ingredient in a product's recipe, set `quantity_normal`, `quantity_maxi`, `is_removable`, `is_addable`, `extra_price_cents`. Delete-and-reinsert pattern within transaction. |
|
||||||
|
| **[RG-ALLERGEN]** | Manage `ingredient_allergen`: INSERT or DELETE `(ingredient_id, allergen_id)` pairs. Allergen list is read-only (14 rows fixed by EU regulation 1169/2011). |
|
||||||
|
| **[POST-1]** | `ingredient` / `product_ingredient` / `ingredient_allergen` rows updated |
|
||||||
|
| **[OUT-1]** | Confirmation, redirect to ingredient list or product composition form |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Domain 7 — Stock management
|
||||||
|
|
||||||
|
### 9.1 RESTOCK
|
||||||
|
|
||||||
|
**Corresponds to MCT section 9.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `stock.manage` |
|
||||||
|
| **[PRE-2]** | Target ingredient exists and `is_active = 1` |
|
||||||
|
| **[PRE-3]** | Number of packs `N >= 1` |
|
||||||
|
| **[RG-1]** | `delta = N * ingredient.pack_size` |
|
||||||
|
| **[RG-2]** | Transaction: `UPDATE ingredient SET stock_quantity = stock_quantity + :delta WHERE id = :id`; INSERT `stock_movement` (ingredient_id, movement_type=`restock`, delta=+delta, order_id=NULL, user_id=actor, note=optional) |
|
||||||
|
| **[RG-3]** | `stock_movement` is append-only: no UPDATE or DELETE on this table (corrections are new rows) |
|
||||||
|
| **[POST-1]** | `ingredient.stock_quantity` incremented by `delta`. One `stock_movement` row of type `restock` inserted. |
|
||||||
|
| **[OUT-1]** | Confirmation with new stock level displayed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.2 INVENTORY_COUNT
|
||||||
|
|
||||||
|
**Corresponds to MCT section 9.2**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `stock.count` |
|
||||||
|
| **[PRE-2]** | Target ingredient exists |
|
||||||
|
| **[PRE-3]** | `actual_quantity >= 0` (physical count is non-negative) |
|
||||||
|
| **[RG-1]** | `delta = actual_quantity - ingredient.stock_quantity` (may be negative if actual < theoretical) |
|
||||||
|
| **[RG-2]** | Transaction: `UPDATE ingredient SET stock_quantity = :actual_quantity WHERE id = :id`; INSERT `stock_movement` (ingredient_id, movement_type=`inventory_correction`, delta=computed, order_id=NULL, user_id=actor, note=optional) |
|
||||||
|
| **[RG-3]** | `delta = 0` is a valid correction (physical count matches theoretical); a movement row is still inserted for audit completeness |
|
||||||
|
| **[POST-1]** | `ingredient.stock_quantity = actual_quantity`. One `stock_movement` row of type `inventory_correction` inserted. |
|
||||||
|
| **[OUT-1]** | Confirmation with reconciled stock level and discrepancy displayed |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9.3 READ_STOCK
|
||||||
|
|
||||||
|
**Corresponds to MCT section 9.3**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `stock.read` |
|
||||||
|
| **[RG-1]** | `SELECT * FROM ingredient WHERE is_active = 1 ORDER BY name ASC` |
|
||||||
|
| **[RG-2]** | Low-stock alert computed at render time: `stock_quantity <= low_stock_threshold` -> flag `low_stock: true` in response. Not stored as a column. |
|
||||||
|
| **[RG-3]** | Optional movement history for a given ingredient: `SELECT * FROM stock_movement WHERE ingredient_id = :id ORDER BY created_at DESC LIMIT :n` |
|
||||||
|
| **[POST-1]** | No database write |
|
||||||
|
| **[OUT-1]** | Ingredient list with `stock_quantity`, `low_stock_threshold`, `pack_size`, `pack_label`, `low_stock` flag |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Domain 8 — User and role management
|
||||||
|
|
||||||
|
### 10.1 CREATE_USER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 10.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `user.create` |
|
||||||
|
| **[PRE-2]** | Email does not already exist in `user.email` (UNIQUE constraint) |
|
||||||
|
| **[PRE-3]** | `role_id` references an existing, active role |
|
||||||
|
| **[RG-1]** | Validation: `email` conforms to RFC 5321 (PHP `FILTER_VALIDATE_EMAIL`), `first_name` and `last_name` non-empty, `role_id` valid |
|
||||||
|
| **[RG-2]** | Password hash: `password_hash($password, PASSWORD_ARGON2ID)`. Minimum password length: 8 characters. |
|
||||||
|
| **[RG-3]** | `is_active = 1` by default; `last_login_at = NULL` at creation |
|
||||||
|
| **[POST-1]** | One `user` row with argon2id `password_hash`, valid `role_id` |
|
||||||
|
| **[OUT-1]** | Redirect to user list with success message |
|
||||||
|
| **[ERR-1]** | Duplicate email: message "This email is already in use" |
|
||||||
|
| **[ERR-2]** | Password too short: inline validation message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.2 UPDATE_USER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 10.2**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `user.update` |
|
||||||
|
| **[PRE-2]** | Target `user.id` exists |
|
||||||
|
| **[RG-1]** | If a new password is supplied (non-empty field): rehash via `PASSWORD_ARGON2ID` and replace existing hash |
|
||||||
|
| **[RG-2]** | If password field is empty: existing hash is preserved unchanged |
|
||||||
|
| **[RG-3]** | Email update subject to UNIQUE constraint (pre-check before UPDATE) |
|
||||||
|
| **[POST-1]** | `user` updated, `updated_at` refreshed |
|
||||||
|
| **[OUT-1]** | Redirect with success message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.3 DEACTIVATE_USER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 10.3**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `user.deactivate` |
|
||||||
|
| **[PRE-2]** | Actor is not targeting their own account (`$targetUserId !== $currentUserId`) |
|
||||||
|
| **[RG-1]** | `UPDATE user SET is_active = 0, updated_at = NOW() WHERE id = :id` |
|
||||||
|
| **[RG-2]** | The user's potentially active session is invalidated on next request: middleware checks `user.is_active = 1` on each authenticated request |
|
||||||
|
| **[POST-1]** | `user.is_active = 0`; user cannot log in; history remains intact |
|
||||||
|
| **[OUT-1]** | Redirect with success message |
|
||||||
|
| **[ERR-1]** | Self-deactivation attempt: HTTP 403, `{error: {code: "SELF_DEACTIVATION_FORBIDDEN"}}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10.4 MANAGE_RBAC
|
||||||
|
|
||||||
|
**Corresponds to MCT section 10.4**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `role.manage` |
|
||||||
|
| **[PRE-2]** | Target `role.id` exists (for permission update) or role fields are valid (for role creation) |
|
||||||
|
| **[PRE-3]** | All submitted `permission_id` values exist in the `permission` catalogue |
|
||||||
|
| **[RG-1 — permissions]** | Transaction: `DELETE FROM role_permission WHERE role_id = :id`; INSERT new `(role_id, permission_id)` pairs for each selected permission |
|
||||||
|
| **[RG-2]** | Permissions are not modifiable via this operation: they are read-only to populate the selection form. Permission catalogue is frozen at seed. |
|
||||||
|
| **[RG-3]** | Effect is immediate for new requests; sessions of users bearing this role see the change on the next permission check (sessions store `role_id`; permissions are reloaded from DB on each check). |
|
||||||
|
| **[RG-4 — custom role]** | Creating a custom role: INSERT `role` (code UNIQUE, label, description, default_route nullable, order_source nullable); INSERT `role_visible_source` rows as needed. |
|
||||||
|
| **[RG-5 — order_source]** | `role.order_source` controls the auto-tagging of `customer_order.source` when this role creates an order. NULL for admin and manager (they can create on behalf of any channel). |
|
||||||
|
| **[POST-1]** | `role_permission` reflects exactly the selected permissions for this role |
|
||||||
|
| **[OUT-1]** | Redirect with success message |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Domain 9 — Stats and KPI
|
||||||
|
|
||||||
|
### 11.1 READ_STATS
|
||||||
|
|
||||||
|
**Corresponds to MCT section 11.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Actor authenticated, holds permission `stats.read` |
|
||||||
|
| **[RG-1 — service_day]** | `service_day` expression used in all stats aggregations: `CASE WHEN HOUR(customer_order.created_at) < 10 THEN DATE(customer_order.created_at) - INTERVAL 1 DAY ELSE DATE(customer_order.created_at) END`. Cutoff at 10:00. No stored column. The v0.1 formula with `INTERVAL 4 HOUR 30 MINUTE` is dropped. |
|
||||||
|
| **[RG-2 — revenue]** | Revenue queries filter `status != 'cancelled'`; they sum `total_ttc_cents` from `customer_order`. Cancelled orders are excluded from revenue but appear in volume counts with `status = 'cancelled'` filter. |
|
||||||
|
| **[RG-3 — top products]** | `SELECT label_snapshot, SUM(quantity) AS total_sold FROM order_item JOIN customer_order ON ... WHERE customer_order.status != 'cancelled' GROUP BY label_snapshot ORDER BY total_sold DESC LIMIT 10` |
|
||||||
|
| **[RG-4 — delivery time KPI]** | Average delivery time: `AVG(TIMESTAMPDIFF(SECOND, paid_at, delivered_at))` on orders with `status = 'delivered'`. SLA reference approx. 10 min (configurable). |
|
||||||
|
| **[RG-5 — breakdown]** | Breakdowns available by `source` (kiosk/counter/drive) and `service_mode` (dine_in/takeaway/drive) for capacity planning. `service_mode` carries no fiscal role (see dictionary note 9). |
|
||||||
|
| **[POST-1]** | No database write |
|
||||||
|
| **[OUT-1]** | Stats dashboard data: revenue by service_day, order counts, top products, cancellation rate, average delivery time, breakdown by source/service_mode |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Domain 10 — Back-office authentication
|
||||||
|
|
||||||
|
### 12.1 AUTHENTICATE_USER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 12.1**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Login form submitted with email and password |
|
||||||
|
| **[PRE-2]** | CSRF token of the form is valid (anti-CSRF protection) |
|
||||||
|
| **[RG-1]** | Lookup: `SELECT * FROM user WHERE email = :email AND is_active = 1 LIMIT 1` |
|
||||||
|
| **[RG-2]** | Password verification: `password_verify($password, $user->password_hash)`. On failure: same generic error whether the email does not exist or the password is wrong (protection against email enumeration). |
|
||||||
|
| **[RG-3]** | On success: `session_regenerate(true)` (session ID regeneration, protection against session fixation) |
|
||||||
|
| **[RG-4]** | Session storage: `$_SESSION['user_id']`, `$_SESSION['role_id']`, `$_SESSION['logged_in_at']` |
|
||||||
|
| **[RG-5]** | UPDATE: `UPDATE user SET last_login_at = NOW() WHERE id = :id` |
|
||||||
|
| **[RG-6]** | Session timeouts: idle timeout 4h (detection via last-activity timestamp in session); absolute timeout 10h (detection via `logged_in_at`) |
|
||||||
|
| **[RG-7]** | Redirect target is `role.default_route` (dynamic; no hardcoded role name in routing logic) |
|
||||||
|
| **[POST-1]** | PHP session open with `user_id` and `role_id`; `user.last_login_at` updated |
|
||||||
|
| **[OUT-1]** | Redirect to `role.default_route` |
|
||||||
|
| **[ERR-1]** | Incorrect credentials or inactive account: generic message "Email or password incorrect" (no distinction to prevent enumeration) |
|
||||||
|
| **[ERR-2]** | Invalid CSRF token: HTTP 403 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12.2 LOGOUT_USER
|
||||||
|
|
||||||
|
**Corresponds to MCT section 12.2**
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[PRE-1]** | Valid session open (`session_id()` non-empty, `$_SESSION['user_id']` present) |
|
||||||
|
| **[RG-1]** | `$_SESSION = []` (clear session data) |
|
||||||
|
| **[RG-2]** | If session cookie exists, expire it: `setcookie(session_name(), '', time() - 3600, '/', '', true, true)` |
|
||||||
|
| **[RG-3]** | `session_destroy()` |
|
||||||
|
| **[POST-1]** | PHP session destroyed; no authenticated access possible with the old cookie |
|
||||||
|
| **[OUT-1]** | Redirect to login page |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Automated treatments — Crons (outside user interactions)
|
||||||
|
|
||||||
|
These treatments are executed by the `wakdo-cron` service container in the maintenance
|
||||||
|
window 01:30-09:30 (outside active service). They are outside the MCT scope (technical
|
||||||
|
treatments, no user trigger) but are documented here for consistency with PROJECT_CONTEXT.
|
||||||
|
|
||||||
|
### 13.1 Stats aggregation (cron 04:30)
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[TRIGGER]** | Cron: `30 4 * * *` |
|
||||||
|
| **[RG-1]** | `service_day` to aggregate: computed per order (see RG-1 of READ_STATS). At 04:30 the service_day in progress is the previous calendar day. |
|
||||||
|
| **[RG-2]** | Aggregations by `service_day`: order count, TTC revenue (sum `total_ttc_cents` where `status != 'cancelled'`), top products (by `label_snapshot`, COUNT in `order_item`) |
|
||||||
|
| **[POST-1]** | Stats available for admin dashboard (direct queries on `customer_order` filtered by `service_day`, or an aggregation table if implemented) |
|
||||||
|
|
||||||
|
### 13.2 Expired sessions purge (cron every 15 min)
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[TRIGGER]** | Cron: `*/15 * * * *` |
|
||||||
|
| **[RG-1]** | File-based sessions (default): `find /tmp/sessions -mmin +240 -delete` |
|
||||||
|
| **[RG-2]** | DB-based sessions (option): `DELETE FROM php_sessions WHERE updated_at < NOW() - INTERVAL 4 HOUR` |
|
||||||
|
| **[POST-1]** | Expired sessions deleted; users inactive for more than 4h are forced to re-login |
|
||||||
|
|
||||||
|
### 13.3 DB backup (cron 03:00)
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[TRIGGER]** | Cron: `0 3 * * *` |
|
||||||
|
| **[RG-1]** | `mysqldump` of the `wakdo` database to a dated file in the backup volume |
|
||||||
|
| **[RG-2]** | Retention: keep the last 7 dumps; delete older ones |
|
||||||
|
| **[POST-1]** | SQL dump available for restoration |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. State machine — consistency recap (MLT)
|
||||||
|
|
||||||
|
Summary of `customer_order.status` transitions covered in the MLT, with corresponding
|
||||||
|
operations, SQL condition, concurrency protection, and phase timestamp set.
|
||||||
|
|
||||||
|
| Transition | MLT operation | SQL condition | Concurrency protection | Phase timestamp set |
|
||||||
|
|------------|--------------|---------------|------------------------|---------------------|
|
||||||
|
| `-> pending_payment` (creation) | CREATE_ORDER (3.3), CREATE_COUNTER_ORDER (4.1) | INSERT with status `pending_payment` | Atomic transaction | `created_at` |
|
||||||
|
| `pending_payment -> paid` | CREATE_ORDER (3.3), CREATE_COUNTER_ORDER (4.1) | UPDATE in same transaction | Atomic transaction | `paid_at` |
|
||||||
|
| `paid -> delivered` | DELIVER_ORDER (6.1) | `WHERE status = 'paid'` | AND status in WHERE | `delivered_at` |
|
||||||
|
| `pending_payment/paid -> cancelled` | CANCEL_ORDER (7.1) | `WHERE status IN ('pending_payment', 'paid')` | AND status IN WHERE | `cancelled_at` |
|
||||||
|
|
||||||
|
Terminal statuses (no further transition defined from these states): `delivered`, `cancelled`.
|
||||||
|
|
||||||
|
**Dropped from v0.1**:
|
||||||
|
- `paid -> preparing` and `preparing -> ready` transitions — intermediate states removed.
|
||||||
|
- MARQUER_EN_PREPARATION (v0.1 MLT section 4.2) — dropped.
|
||||||
|
- MARQUER_PRETE (v0.1 MLT section 4.3) — dropped.
|
||||||
|
- `preparing` and `ready` in the cancellable state set — the cancellable set is now
|
||||||
|
`['pending_payment', 'paid']` only.
|
||||||
|
- `commande_event` table and v0.1 RG-T10 — replaced by phase timestamps on `customer_order`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Residual notes and open points
|
||||||
|
|
||||||
|
### 15.1 `service_day` — not materialised as a column
|
||||||
|
|
||||||
|
The `service_day` computation is documented (RG-2 of CREATE_ORDER, RG-1 of READ_STATS):
|
||||||
|
`CASE WHEN HOUR(created_at) < 10 THEN DATE(created_at) - INTERVAL 1 DAY ELSE DATE(created_at) END`
|
||||||
|
(cutoff 10:00). It is computed at query time, not stored. For high-frequency stats queries,
|
||||||
|
a MariaDB generated column `VIRTUAL` or `STORED` could be added at DDL time to avoid
|
||||||
|
per-row recomputation, but this is not a blocker for the RNCP scope.
|
||||||
|
The v0.1 formula with `INTERVAL 4 HOUR 30 MINUTE` was incorrect and is dropped.
|
||||||
|
|
||||||
|
### 15.2 `order_item_modifier` for menu items
|
||||||
|
|
||||||
|
For a menu line (`item_type='menu'`), modifiers target the fixed burger identified via
|
||||||
|
`order_item.menu_id -> menu.burger_product_id`. The constraint that modifiers reference
|
||||||
|
only ingredients belonging to the burger's `product_ingredient` is enforced at the
|
||||||
|
application layer, not at the DB FK layer (see dictionary note 10). This is a known
|
||||||
|
trade-off: a multi-column FK or a DB trigger would be needed to enforce it at DB level.
|
||||||
|
Documenting it as an application invariant is the retained approach for this project scope.
|
||||||
|
|
||||||
|
### 15.3 Order number NNN counter — concurrency
|
||||||
|
|
||||||
|
The sequential NNN counter per `(source, service_day)` could produce duplicates under
|
||||||
|
high concurrency if implemented naively as `SELECT COUNT + 1`. The recommended
|
||||||
|
implementation at DDL/code time is either: (a) a table-level advisory lock around the
|
||||||
|
count-and-insert sequence; or (b) a dedicated sequence table with an atomic increment.
|
||||||
|
The UNIQUE constraint on `order_number` provides the last-resort guard (INSERT would fail
|
||||||
|
and the application retries). This is not a blocker for the RNCP demo volume.
|
||||||
193
docs/uml/sequence-passer-commande.md
Normal file
193
docs/uml/sequence-passer-commande.md
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
# Diagramme de sequence - Passer une commande (borne client)
|
||||||
|
|
||||||
|
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
||||||
|
**Statut** : v0.1
|
||||||
|
**Date** : 2026-05-21
|
||||||
|
**Branche** : `feat/p1-conception`
|
||||||
|
**Auteur methodologie** : BYAN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objet du document
|
||||||
|
|
||||||
|
Ce document decrit le **flux temporel** du parcours "passer une commande" cote
|
||||||
|
**Client sur la borne kiosk** : navigation dans les categories, selection d'un
|
||||||
|
produit ou composition d'un menu, gestion du panier, validation avec saisie du
|
||||||
|
numero de retrait, paiement, puis confirmation.
|
||||||
|
|
||||||
|
Le diagramme reste au niveau **conceptuel / logique**. Il nomme les echanges
|
||||||
|
entre participants sans detailler l'implementation PHP (controllers, models)
|
||||||
|
ni le SQL exact. Il complete le cas d'utilisation "Passer une commande" de
|
||||||
|
`docs/uml/use-cases.md` et la machine a etats de `docs/uml/state-commande.md`.
|
||||||
|
|
||||||
|
**Sources** :
|
||||||
|
- `docs/PROJECT_CONTEXT.md` section 2 (processus metier), section 7 (endpoints API)
|
||||||
|
- `docs/merise/dictionary.md` (`commande`, `ligne_commande`, `menu`, `produit`)
|
||||||
|
- `docs/uml/state-commande.md` (transitions `pending_payment -> paid`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Participants
|
||||||
|
|
||||||
|
| Participant | Role | Couche |
|
||||||
|
|---|---|---|
|
||||||
|
| **Client** | Utilisateur final, compose sa commande au doigt | Acteur |
|
||||||
|
| **Borne** | Interface tactile (front Bloc 1, HTML/CSS/JS vanilla) | Presentation |
|
||||||
|
| **API** | Back-end REST sous `/api/*` (Bloc 2) | Application |
|
||||||
|
| **BDD** | Base de donnees MariaDB | Persistance |
|
||||||
|
|
||||||
|
Le panier est gere **cote Borne** (etat local du front) jusqu'a la validation.
|
||||||
|
Aucune commande n'est creee en base avant la validation finale, pour eviter les
|
||||||
|
commandes fantomes abandonnees.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Diagramme de sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Client
|
||||||
|
participant Borne
|
||||||
|
participant API
|
||||||
|
participant BDD
|
||||||
|
|
||||||
|
Note over Client,BDD: Phase 1 - Navigation du catalogue
|
||||||
|
|
||||||
|
Client->>Borne: ouvrir la borne
|
||||||
|
Borne->>API: GET /api/categories
|
||||||
|
API->>BDD: lire les categories actives
|
||||||
|
BDD-->>API: liste des categories
|
||||||
|
API-->>Borne: categories (JSON)
|
||||||
|
Borne-->>Client: afficher les categories
|
||||||
|
|
||||||
|
Client->>Borne: choisir une categorie
|
||||||
|
Borne->>API: GET /api/products (filtre categorie)
|
||||||
|
API->>BDD: lire les produits disponibles
|
||||||
|
BDD-->>API: liste des produits
|
||||||
|
API-->>Borne: produits (JSON)
|
||||||
|
Borne-->>Client: afficher les produits
|
||||||
|
|
||||||
|
Note over Client,BDD: Phase 2 - Selection produit ou composition menu
|
||||||
|
|
||||||
|
alt Produit a la carte
|
||||||
|
Client->>Borne: selectionner un produit
|
||||||
|
Client->>Borne: regler taille / options
|
||||||
|
Borne->>Borne: ajouter la ligne au panier local
|
||||||
|
else Composition d'un menu
|
||||||
|
Client->>Borne: selectionner un menu
|
||||||
|
Borne->>API: GET /api/menus (composition du menu)
|
||||||
|
API->>BDD: lire menu et composition
|
||||||
|
BDD-->>API: menu + produits par role
|
||||||
|
API-->>Borne: composition (JSON)
|
||||||
|
Borne-->>Client: afficher les choix par slot (burger, accompagnement, boisson, sauce)
|
||||||
|
Client->>Borne: choisir chaque composant + tailles
|
||||||
|
Borne->>Borne: ajouter la ligne menu au panier local
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over Client,BDD: Phase 3 - Gestion du panier
|
||||||
|
|
||||||
|
Client->>Borne: consulter le panier
|
||||||
|
Borne-->>Client: recapitulatif + total provisoire
|
||||||
|
opt Modifier le panier
|
||||||
|
Client->>Borne: ajuster quantite / supprimer une ligne
|
||||||
|
Borne->>Borne: recalculer le total local
|
||||||
|
Borne-->>Client: panier mis a jour
|
||||||
|
end
|
||||||
|
|
||||||
|
Note over Client,BDD: Phase 4 - Validation du panier et saisie du numero
|
||||||
|
|
||||||
|
Client->>Borne: valider la commande
|
||||||
|
Client->>Borne: saisir le numero de retrait
|
||||||
|
Borne->>Borne: valider le panier (au moins 1 ligne)
|
||||||
|
Borne->>API: POST /api/orders (lignes + mode_consommation + numero)
|
||||||
|
|
||||||
|
API->>API: recalculer les totaux cote serveur
|
||||||
|
API->>BDD: creer la commande (statut pending_payment)
|
||||||
|
API->>BDD: creer les lignes (snapshot libelle + prix)
|
||||||
|
BDD-->>API: commande persistee {id, numero, statut: pending_payment}
|
||||||
|
API-->>Borne: 201 Created {id, numero, statut: pending_payment, total}
|
||||||
|
Borne-->>Client: afficher le total a regler
|
||||||
|
|
||||||
|
Note over Client,BDD: Phase 5 - Paiement (pending_payment -> paid)
|
||||||
|
|
||||||
|
Client->>Borne: payer la commande
|
||||||
|
Borne->>API: POST /api/orders/{id}/pay
|
||||||
|
API->>BDD: enregistrer le paiement, passer la commande a paid (paye_a)
|
||||||
|
BDD-->>API: commande mise a jour {id, numero, statut: paid}
|
||||||
|
|
||||||
|
Note over Client,BDD: Phase 6 - Confirmation
|
||||||
|
|
||||||
|
API-->>Borne: 200 OK {id, numero, statut: paid}
|
||||||
|
Borne-->>Client: ecran de confirmation avec le numero
|
||||||
|
|
||||||
|
Note over Client,BDD: Cas d'erreur
|
||||||
|
|
||||||
|
alt Panier vide ou donnees invalides
|
||||||
|
API-->>Borne: 4xx {error: code, message}
|
||||||
|
Borne-->>Client: message d'erreur, retour au panier
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Notes de modelisation
|
||||||
|
|
||||||
|
### 4.1 Recalcul des totaux cote serveur
|
||||||
|
|
||||||
|
La Borne affiche un total **provisoire** calcule localement pour l'experience
|
||||||
|
utilisateur. L'API recalcule les totaux a la reception du `POST /api/orders` a
|
||||||
|
partir des prix en base, puis fige les snapshots
|
||||||
|
(`prix_unitaire_ttc_cents_snapshot`, `libelle_snapshot` dans `ligne_commande`,
|
||||||
|
voir `dictionary.md` 3.6). Le total affiche par le client n'est pas considere
|
||||||
|
comme la source de verite : ceci limite la falsification du prix cote client.
|
||||||
|
|
||||||
|
### 4.2 Transitions de statut
|
||||||
|
|
||||||
|
Le parcours materialise les transitions T1 et T2 de
|
||||||
|
`docs/uml/state-commande.md`, en deux phases successives conformes a la regle
|
||||||
|
metier :
|
||||||
|
|
||||||
|
- `POST /api/orders` cree la commande composee en `pending_payment` (T1).
|
||||||
|
- `POST /api/orders/{id}/pay` enregistre le paiement et fait passer la commande
|
||||||
|
a `paid` (T2), avec l'horodatage `paye_a`.
|
||||||
|
|
||||||
|
La separation des deux appels reflete les deux phases du cycle de vie :
|
||||||
|
composer la commande, puis la payer.
|
||||||
|
|
||||||
|
### 4.3 Panier local jusqu'a la validation
|
||||||
|
|
||||||
|
Aucun appel ecriture vers la BDD n'a lieu pendant les phases 1 a 3. Le panier
|
||||||
|
vit dans l'etat du front (JavaScript). Ce choix evite de creer en base des
|
||||||
|
commandes abandonnees et reduit le nombre d'ecritures. Inconvenient connu : un
|
||||||
|
rafraichissement de la borne peut vider le panier ; un stockage local cote
|
||||||
|
navigateur peut etre envisage plus tard.
|
||||||
|
|
||||||
|
### 4.4 Fallback JSON (hors flux nominal)
|
||||||
|
|
||||||
|
`PROJECT_CONTEXT.md` section 4 prevoit un mode de repli ou la Borne lit des
|
||||||
|
fichiers JSON statiques si l'API est indisponible. Ce mode concerne uniquement
|
||||||
|
les lectures (phases 1 a 2). La validation (phase 4) et le paiement (phase 5)
|
||||||
|
requierent l'API ; sans elle, la commande n'est ni persistee ni payee. Ce cas
|
||||||
|
degrade n'est pas detaille dans le diagramme nominal ci-dessus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Coherence avec les autres livrables
|
||||||
|
|
||||||
|
| Verification | Resultat |
|
||||||
|
|---|---|
|
||||||
|
| Endpoints utilises existent dans `PROJECT_CONTEXT.md` section 7 | `GET /api/categories`, `GET /api/products`, `GET /api/menus`, `POST /api/orders` ; `POST /api/orders/{id}/pay` est a confirmer en section 7 du brief |
|
||||||
|
| Entites manipulees presentes au MCD | Oui : `categorie`, `produit`, `menu`, `menu_produit`, `commande`, `ligne_commande` |
|
||||||
|
| Statuts utilises coherents avec `state-commande.md` | Oui : `pending_payment` puis `paid` (T1, T2), valeurs ENUM anglaises |
|
||||||
|
| Format de reponse JSON | Coherent avec `PROJECT_CONTEXT.md` section 7 (`{data, error}`) et la reponse `{id, number, status}` du POST orders |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Arbitrage tranche
|
||||||
|
|
||||||
|
La phase de paiement est integree au flux conformement a la regle metier des
|
||||||
|
deux phases (composer puis payer). La sequence suit la machine canonique de
|
||||||
|
`state-commande.md` : creation en `pending_payment` (T1) puis paiement vers
|
||||||
|
`paid` (T2), avec des valeurs ENUM en anglais. Point a confirmer au MCT :
|
||||||
|
l'endpoint de paiement (`POST /api/orders/{id}/pay`) doit etre reporte dans la
|
||||||
|
section 7 du brief s'il n'y figure pas encore.
|
||||||
144
docs/uml/state-commande.md
Normal file
144
docs/uml/state-commande.md
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
# Diagramme d'etats-transitions - Commande
|
||||||
|
|
||||||
|
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
||||||
|
**Statut** : v0.1
|
||||||
|
**Date** : 2026-05-21
|
||||||
|
**Branche** : `feat/p1-conception`
|
||||||
|
**Auteur methodologie** : BYAN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objet du document
|
||||||
|
|
||||||
|
Ce document formalise la **machine a etats** de l'attribut `commande.statut`.
|
||||||
|
Il decrit les etats possibles d'une commande, les transitions autorisees entre
|
||||||
|
ces etats, les **evenements** qui les declenchent et les **gardes** (conditions)
|
||||||
|
qui les conditionnent.
|
||||||
|
|
||||||
|
Il complete le MCD (`docs/merise/mcd.md` section 9, qui esquisse le cycle de
|
||||||
|
vie) et le dictionnaire (`docs/merise/dictionary.md` 3.5, qui declare l'ENUM).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Source de verite et regle metier
|
||||||
|
|
||||||
|
La regle metier confirmee fixe deux phases successives dans le cycle de vie
|
||||||
|
d'une commande : le client **compose** sa commande, **puis** il **paie**. Une
|
||||||
|
fois payee, la commande entre en preparation. Le paiement fait partie integrante
|
||||||
|
du cycle. Les valeurs d'etat sont en anglais et alignees sur l'ENUM du
|
||||||
|
dictionnaire.
|
||||||
|
|
||||||
|
| Source | Valeurs de statut |
|
||||||
|
|---|---|
|
||||||
|
| `dictionary.md` 3.5 (ENUM SQL) | `pending_payment`, `paid`, `preparing`, `ready`, `delivered`, `cancelled` |
|
||||||
|
| Regle metier confirmee | composer -> payer -> preparer -> pret -> remettre |
|
||||||
|
|
||||||
|
**Machine a etats canonique** : la machine ci-dessous est la seule autorisee.
|
||||||
|
Elle suit l'ENUM du dictionnaire et la regle metier des deux phases :
|
||||||
|
|
||||||
|
- `pending_payment` : commande composee, en attente de paiement.
|
||||||
|
- `paid` : paiement effectue ; la commande peut entrer en file de preparation.
|
||||||
|
|
||||||
|
> Le dictionnaire (`dictionary.md` 3.5) et la machine ci-dessous partagent la
|
||||||
|
> meme ENUM, ce qui maintient la coherence entre le modele de donnees et le
|
||||||
|
> modele d'etats (cross-validation, mantra #34).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Etats retenus
|
||||||
|
|
||||||
|
| Etat | Valeur ENUM | Signification | Acteur qui declenche l'entree |
|
||||||
|
|---|---|---|---|
|
||||||
|
| En attente de paiement | `pending_payment` | Commande composee, panier fige, en attente de paiement. | Client (kiosk) ou Accueil (counter/drive) |
|
||||||
|
| Payee | `paid` | Paiement effectue ; la commande peut entrer en file de preparation. | Client (paiement) ou Accueil |
|
||||||
|
| En preparation | `preparing` | Prise en charge par la Preparation, en cuisine. | Preparation |
|
||||||
|
| Prete | `ready` | Preparation terminee, prete au comptoir. | Preparation |
|
||||||
|
| Livree | `delivered` | Remise effectuee au client. Etat **final**. | Accueil |
|
||||||
|
| Annulee | `cancelled` | Commande abandonnee ou annulee. Etat **final**. | Client, Accueil ou Administration |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Diagramme d'etats-transitions
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> pending_payment : creer commande (kiosk / counter / drive)
|
||||||
|
|
||||||
|
pending_payment --> paid : payer\n[panier contient au moins 1 ligne]
|
||||||
|
pending_payment --> cancelled : abandonner\n[avant paiement]
|
||||||
|
|
||||||
|
paid --> preparing : prendre en charge\n[acteur Preparation, file triee par heure croissante]
|
||||||
|
paid --> cancelled : annuler\n[Accueil ou Administration]
|
||||||
|
|
||||||
|
preparing --> ready : declarer preparee\n[acteur Preparation]
|
||||||
|
preparing --> cancelled : annuler\n[rupture produit / decision Administration]
|
||||||
|
|
||||||
|
ready --> delivered : remettre au client\n[acteur Accueil]
|
||||||
|
ready --> cancelled : annuler\n[client absent / non recuperee]
|
||||||
|
|
||||||
|
delivered --> [*]
|
||||||
|
cancelled --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Transitions detaillees
|
||||||
|
|
||||||
|
| # | De | Vers | Evenement declencheur | Garde (condition) | Acteur |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| T1 | (initial) | `pending_payment` | Creation de la commande composee | Au moins un item ajoute au panier en cours | Client / Accueil |
|
||||||
|
| T2 | `pending_payment` | `paid` | Paiement de la commande | La commande contient au moins une `ligne_commande` ; le paiement aboutit | Client / Accueil |
|
||||||
|
| T3 | `pending_payment` | `cancelled` | Abandon avant paiement | Commande pas encore payee | Client / Accueil |
|
||||||
|
| T4 | `paid` | `preparing` | Prise en charge en file | La commande est la plus ancienne non traitee (tri par heure de livraison croissante) | Preparation |
|
||||||
|
| T5 | `paid` | `cancelled` | Annulation avant preparation | Decision operationnelle | Accueil / Administration |
|
||||||
|
| T6 | `preparing` | `ready` | Declaration "preparee" | Preparation terminee | Preparation |
|
||||||
|
| T7 | `preparing` | `cancelled` | Annulation pendant preparation | Rupture produit ou decision Administration | Preparation / Administration |
|
||||||
|
| T8 | `ready` | `delivered` | Remise physique au client | Le client se presente avec le bon numero | Accueil |
|
||||||
|
| T9 | `ready` | `cancelled` | Annulation apres preparation | Client non present / commande non recuperee | Accueil / Administration |
|
||||||
|
|
||||||
|
### Invariants de la machine a etats
|
||||||
|
|
||||||
|
- `delivered` et `cancelled` sont des etats **finaux** : aucune transition n'en
|
||||||
|
sort.
|
||||||
|
- Aucune transition ne revient en arriere (pas de `preparing -> paid`). Une
|
||||||
|
erreur operationnelle se traite par annulation puis nouvelle commande, pour
|
||||||
|
preserver l'integrite de l'historique et des snapshots de prix.
|
||||||
|
- La transition vers `cancelled` est possible depuis tous les etats **sauf**
|
||||||
|
`delivered` (une commande remise ne s'annule pas dans ce modele). Ceci est
|
||||||
|
coherent avec `mcd.md` section 9 : "Annuler : transition vers `cancelled`
|
||||||
|
(depuis tout statut sauf `delivered`)".
|
||||||
|
- `paye_a` (DATETIME, `dictionary.md` 3.5) est renseigne au moment de la
|
||||||
|
transition T2 (`pending_payment -> paid`) et reste NULL avant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Coherence avec les autres livrables
|
||||||
|
|
||||||
|
| Verification | Resultat |
|
||||||
|
|---|---|
|
||||||
|
| Tous les etats du diagramme existent dans l'ENUM `dictionary.md` 3.5 | Oui (6 valeurs, toutes utilisees) |
|
||||||
|
| La regle "annulation possible sauf depuis delivered" de `mcd.md` 9 | Respectee (T5, T7, T9 ; pas de transition depuis `delivered`) |
|
||||||
|
| Cycle de vie esquisse dans `mcd.md` 9 | Couvert : `pending_payment` -> `paid` (payer), `paid` -> `preparing` (preparer), `preparing` -> `ready` (marquer pret), `ready` -> `delivered` (remettre) |
|
||||||
|
| Acteurs de `use-cases.md` | Preparation declenche T4/T6/T7 ; Accueil declenche T8/T9 ; Administration peut annuler |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Arbitrage tranche
|
||||||
|
|
||||||
|
La divergence historique entre l'ENUM du dictionnaire et un parcours sans
|
||||||
|
paiement est resolue par la regle metier confirmee : le cycle de vie comporte
|
||||||
|
deux phases successives, la composition de la commande puis son paiement. Le
|
||||||
|
paiement fait partie integrante du cycle.
|
||||||
|
|
||||||
|
La machine canonique retenue est donc :
|
||||||
|
|
||||||
|
```
|
||||||
|
pending_payment -> paid -> preparing -> ready -> delivered
|
||||||
|
(cancelled atteignable depuis pending_payment, paid, preparing)
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette machine est la source de verite partagee par `dictionary.md` 3.5,
|
||||||
|
`use-cases.md` (cas "Payer la commande" cote Client) et
|
||||||
|
`sequence-passer-commande.md` (etape paiement entre validation du panier et
|
||||||
|
confirmation). La colonne `paye_a` est renseignee a la transition T2. A
|
||||||
|
revalider lors du MCT.
|
||||||
222
docs/uml/use-cases.md
Normal file
222
docs/uml/use-cases.md
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
# Diagramme de cas d'utilisation - Wakdo
|
||||||
|
|
||||||
|
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
||||||
|
**Statut** : v0.1
|
||||||
|
**Date** : 2026-05-21
|
||||||
|
**Branche** : `feat/p1-conception`
|
||||||
|
**Auteur methodologie** : BYAN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Objet du document
|
||||||
|
|
||||||
|
Ce document recense les **cas d'utilisation** de Wakdo, c'est-a-dire les
|
||||||
|
fonctionnalites observables du systeme du point de vue de ses acteurs. Il
|
||||||
|
complete le MCD (`docs/merise/mcd.md`) et le dictionnaire
|
||||||
|
(`docs/merise/dictionary.md`) en passant de la vue **donnees** a la vue
|
||||||
|
**usages**.
|
||||||
|
|
||||||
|
Le diagramme reste au niveau conceptuel. Il ne prejuge pas de l'ecran ou de
|
||||||
|
l'endpoint qui realisera chaque cas, mais identifie qui fait quoi.
|
||||||
|
|
||||||
|
**Sources** :
|
||||||
|
- `docs/PROJECT_CONTEXT.md` sections 2 (acteurs, processus), 7 (scope RBAC)
|
||||||
|
- `docs/merise/dictionary.md` (entites `commande`, `role`, `user`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Acteurs - perimetre et challenge de pertinence
|
||||||
|
|
||||||
|
Le brief (`PROJECT_CONTEXT.md` section 2 et section 7) definit les acteurs
|
||||||
|
metier. Avant de les retenir, chaque acteur propose dans la consigne initiale
|
||||||
|
est confronte au perimetre reel du projet.
|
||||||
|
|
||||||
|
| Acteur candidat | Statut | Justification (perimetre reel) |
|
||||||
|
|---|---|---|
|
||||||
|
| **Client (borne kiosk)** | Retenu | Acteur central du Bloc 1. Compose et valide une commande sur la borne tactile autonome (canal `kiosk`). Non authentifie. |
|
||||||
|
| **Manager / Admin** | Retenu, fusionne en **Administration** | Le brief ne distingue pas "manager" et "admin" comme deux roles. Le role RBAC reel est `admin` (section 7). Il porte le CRUD catalogue, la gestion des utilisateurs/roles et les stats. On nomme l'acteur **Administration** pour coller au vocabulaire du brief. |
|
||||||
|
| **Cuisine** | Retenu, renomme **Preparation** | Correspond au role RBAC `preparation` (section 7). Voit la file des commandes a preparer triees par heure de livraison croissante et fait avancer leur statut. Le terme "Cuisine" est un synonyme metier ; le role technique est `preparation`. |
|
||||||
|
| **Caisse** | Ecarte comme acteur distinct | Challenge : il n'existe pas de role RBAC `caisse` (les 3 roles sont `admin`, `preparation`, `accueil`). Le paiement existe dans le cycle (cote Client sur la borne et cote Accueil au comptoir/drive), mais aucun acteur "Caisse" dedie n'est modelise. L'equivalent operationnel le plus proche est l'**Accueil** (role `accueil`) qui saisit les commandes au comptoir/drive et remet les commandes livrees. |
|
||||||
|
| **Accueil** | Retenu (non liste dans la consigne mais present au brief) | Role RBAC `accueil`. Saisit les commandes au comptoir (canal `counter`) ou au drive (canal `drive`), puis remet les commandes au client (passage a `delivered`). C'est l'acteur qui recouvre le besoin que la consigne attribuait a "Caisse". |
|
||||||
|
|
||||||
|
### Decision sur les acteurs retenus
|
||||||
|
|
||||||
|
Quatre acteurs sont conserves au diagramme :
|
||||||
|
|
||||||
|
1. **Client** (borne, non authentifie)
|
||||||
|
2. **Administration** (role `admin`)
|
||||||
|
3. **Preparation** (role `preparation`, ex-"Cuisine")
|
||||||
|
4. **Accueil** (role `accueil`, recouvre le besoin "Caisse")
|
||||||
|
|
||||||
|
> Decision actee : il n'y a **pas** de parcours employe dedie modelise a part.
|
||||||
|
> Les cas d'usage des employes (Administration, Preparation, Accueil) sont
|
||||||
|
> couverts directement ici. Cette decision suit le mantra du Rasoir d'Ockham
|
||||||
|
> (#37) : on evite une couche de modelisation redondante tant qu'aucun besoin
|
||||||
|
> ne la justifie.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Diagramme de cas d'utilisation
|
||||||
|
|
||||||
|
Mermaid ne fournit pas de type `usecase` natif. La representation ci-dessous
|
||||||
|
utilise un `flowchart` : les acteurs sont des noeuds a gauche, les cas
|
||||||
|
d'utilisation sont des noeuds arrondis regroupes par sous-systeme, et les
|
||||||
|
fleches portent les relations (`<<include>>`, `<<extend>>`) la ou elles
|
||||||
|
ont du sens.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
%% Acteurs
|
||||||
|
Client(("Client<br/>borne kiosk"))
|
||||||
|
Admin(("Administration<br/>role admin"))
|
||||||
|
Prep(("Preparation<br/>role preparation"))
|
||||||
|
Accueil(("Accueil<br/>role accueil"))
|
||||||
|
|
||||||
|
%% Sous-systeme Borne client
|
||||||
|
subgraph BORNE["Borne client - Bloc 1"]
|
||||||
|
UC1(["Consulter le catalogue"])
|
||||||
|
UC2(["Composer un menu"])
|
||||||
|
UC3(["Passer une commande"])
|
||||||
|
UC4(["Saisir le numero de retrait"])
|
||||||
|
UC5(["Recevoir la confirmation"])
|
||||||
|
UC6(["Payer la commande"])
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Sous-systeme Back-office
|
||||||
|
subgraph BACK["Back-office - Bloc 2"]
|
||||||
|
UC10(["Gerer le catalogue<br/>categories, produits, menus"])
|
||||||
|
UC11(["Gerer les utilisateurs et roles"])
|
||||||
|
UC12(["Consulter les statistiques"])
|
||||||
|
UC20(["Consulter la file de preparation"])
|
||||||
|
UC21(["Faire avancer une commande"])
|
||||||
|
UC30(["Saisir une commande<br/>comptoir ou drive"])
|
||||||
|
UC31(["Remettre la commande au client"])
|
||||||
|
UC40(["S'authentifier"])
|
||||||
|
end
|
||||||
|
|
||||||
|
%% Relations Client
|
||||||
|
Client --> UC1
|
||||||
|
Client --> UC2
|
||||||
|
Client --> UC3
|
||||||
|
Client --> UC6
|
||||||
|
Client --> UC5
|
||||||
|
|
||||||
|
%% include / extend cote borne
|
||||||
|
UC3 -. include .-> UC4
|
||||||
|
UC3 -. include .-> UC6
|
||||||
|
UC2 -. include .-> UC1
|
||||||
|
UC3 -. extend .-> UC2
|
||||||
|
|
||||||
|
%% Relations Administration
|
||||||
|
Admin --> UC40
|
||||||
|
Admin --> UC10
|
||||||
|
Admin --> UC11
|
||||||
|
Admin --> UC12
|
||||||
|
|
||||||
|
%% Relations Preparation
|
||||||
|
Prep --> UC40
|
||||||
|
Prep --> UC20
|
||||||
|
Prep --> UC21
|
||||||
|
|
||||||
|
%% Relations Accueil
|
||||||
|
Accueil --> UC40
|
||||||
|
Accueil --> UC30
|
||||||
|
Accueil --> UC31
|
||||||
|
UC30 -. include .-> UC1
|
||||||
|
|
||||||
|
%% Authentification mutualisee
|
||||||
|
UC10 -. include .-> UC40
|
||||||
|
UC11 -. include .-> UC40
|
||||||
|
UC20 -. include .-> UC40
|
||||||
|
UC30 -. include .-> UC40
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Description des cas d'utilisation
|
||||||
|
|
||||||
|
### 4.1 Acteur Client (borne kiosk)
|
||||||
|
|
||||||
|
| Cas | Description | Entites manipulees |
|
||||||
|
|---|---|---|
|
||||||
|
| Consulter le catalogue | Parcourir les categories, produits et menus disponibles. Charges via `GET /api/categories`, `/api/products`, `/api/menus` (ou JSON fallback). | `categorie`, `produit`, `menu` |
|
||||||
|
| Composer un menu | Choisir burger + accompagnement + boisson + sauce, regler les options de taille (normale / grande) et de personnalisation. Etend "Passer une commande" car un menu compose est une variante d'item au panier. | `menu`, `menu_produit`, `produit` |
|
||||||
|
| Passer une commande | Valider le panier, declencher la creation de la commande composee. Inclut la saisie du numero de retrait et le paiement. | `commande`, `ligne_commande` |
|
||||||
|
| Saisir le numero de retrait | Renseigner le numero qui identifie le client au comptoir. Cas inclus par "Passer une commande". | `commande.numero` |
|
||||||
|
| Payer la commande | Regler la commande une fois le panier compose et valide. Materialise la transition `pending_payment -> paid` de `state-commande.md`. Cas inclus par "Passer une commande". | `commande.statut`, `commande.paye_a` |
|
||||||
|
| Recevoir la confirmation | Afficher l'ecran de confirmation avec le numero, apres paiement. | `commande` |
|
||||||
|
|
||||||
|
> Note de coherence : le cycle de vie comporte deux phases successives, la
|
||||||
|
> composition de la commande puis son paiement (regle metier confirmee). Le cas
|
||||||
|
> "Payer la commande" est retenu cote Client et materialise la transition
|
||||||
|
> `pending_payment -> paid` de l'ENUM `statut`
|
||||||
|
> (`dictionary.md` 3.5, `state-commande.md`).
|
||||||
|
|
||||||
|
### 4.2 Acteur Administration (role admin)
|
||||||
|
|
||||||
|
| Cas | Description | Entites manipulees |
|
||||||
|
|---|---|---|
|
||||||
|
| Gerer le catalogue | CRUD sur categories, produits et menus (libelles, prix, images, disponibilite, composition de menu). | `categorie`, `produit`, `menu`, `menu_produit` |
|
||||||
|
| Gerer les utilisateurs et roles | CRUD sur les comptes back-office et leurs roles ; consultation de la matrice de permissions. | `user`, `role`, `permission`, `role_permission` |
|
||||||
|
| Consulter les statistiques | Voir les commandes du jour de service, le chiffre d'affaires, les produits les plus commandes. | `commande`, `ligne_commande` |
|
||||||
|
|
||||||
|
### 4.3 Acteur Preparation (role preparation, ex-Cuisine)
|
||||||
|
|
||||||
|
| Cas | Description | Entites manipulees |
|
||||||
|
|---|---|---|
|
||||||
|
| Consulter la file de preparation | Afficher les commandes a preparer triees par heure de livraison croissante, tous canaux confondus. | `commande`, `ligne_commande` |
|
||||||
|
| Faire avancer une commande | Declarer une commande "preparee", ce qui declenche une transition de statut (voir `state-commande.md`). | `commande.statut` |
|
||||||
|
|
||||||
|
### 4.4 Acteur Accueil (role accueil, recouvre Caisse)
|
||||||
|
|
||||||
|
| Cas | Description | Entites manipulees |
|
||||||
|
|---|---|---|
|
||||||
|
| Saisir une commande | Creer une commande pour un client au comptoir (`counter`) ou au drive (`drive`), en consultant le catalogue. | `commande`, `ligne_commande`, `produit`, `menu` |
|
||||||
|
| Remettre la commande au client | Declarer une commande "livree" au moment de la remise physique. | `commande.statut` |
|
||||||
|
|
||||||
|
### 4.5 Cas transverse - S'authentifier
|
||||||
|
|
||||||
|
Tous les acteurs du back-office (Administration, Preparation, Accueil) passent
|
||||||
|
par "S'authentifier" avant d'acceder a leurs cas. Modelise comme cas inclus
|
||||||
|
(`<<include>>`) par chaque cas back-office pour eviter de surcharger le
|
||||||
|
diagramme. Le Client de la borne n'est pas authentifie (canal `kiosk` public).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Relations include / extend retenues
|
||||||
|
|
||||||
|
| Relation | Type | Justification |
|
||||||
|
|---|---|---|
|
||||||
|
| Passer une commande -> Saisir le numero de retrait | include | La saisie du numero fait partie integrante de toute validation de commande. |
|
||||||
|
| Passer une commande -> Payer la commande | include | Le paiement suit la composition du panier et fait partie integrante du parcours (phase 2 du cycle de vie). |
|
||||||
|
| Composer un menu -> Consulter le catalogue | include | Composer un menu suppose de parcourir les produits eligibles a chaque slot. |
|
||||||
|
| Passer une commande -> Composer un menu | extend | Le menu est un cas optionnel : une commande peut ne contenir que des produits a la carte. La composition etend le parcours seulement si le client choisit un menu. |
|
||||||
|
| Saisir une commande (Accueil) -> Consulter le catalogue | include | L'equipier consulte le catalogue pour saisir au comptoir / drive. |
|
||||||
|
| Cas back-office -> S'authentifier | include | Acces conditionne a une session authentifiee. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Incoherences remontees vers les autres livrables
|
||||||
|
|
||||||
|
Ces ecarts entre les sources sont signales pour arbitrage de l'auteur (la
|
||||||
|
modelisation finale releve de sa decision, mantra de validation humaine).
|
||||||
|
|
||||||
|
1. **ENUM `statut` et phase de paiement (tranche)**
|
||||||
|
Le dictionnaire (`dictionary.md` 3.5) definit
|
||||||
|
`statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled')`
|
||||||
|
avec un paiement explicite. La regle metier confirmee fixe deux phases
|
||||||
|
successives, la composition de la commande puis son paiement. Le cas
|
||||||
|
"Payer la commande" est donc retenu cote Client et materialise la transition
|
||||||
|
`pending_payment -> paid`. Cet ecart est tranche : la machine canonique de
|
||||||
|
`state-commande.md` fait foi.
|
||||||
|
|
||||||
|
2. **Acteur "Caisse" absent du RBAC**
|
||||||
|
Aucun role `caisse` n'existe (`PROJECT_CONTEXT.md` section 7 : `admin`,
|
||||||
|
`preparation`, `accueil`). La fonction d'encaissement de la consigne a ete
|
||||||
|
rattachee a l'acteur **Accueil**. A confirmer.
|
||||||
|
|
||||||
|
3. **"Manager" vs "Admin"**
|
||||||
|
La consigne parle de "Manager/Admin" ; le brief ne connait que `admin`. Les
|
||||||
|
deux ont ete fusionnes en un acteur **Administration**. A confirmer si un
|
||||||
|
role manager intermediaire est souhaite (le dictionnaire 3.8 mentionne un
|
||||||
|
role `manager` extensible, non present dans le scope section 7).
|
||||||
Loading…
Add table
Reference in a new issue