Compare commits
No commits in common. "fae5c237226161bfb72039c9022cded1efccb4ee" and "392ba9a04035cde533e4f5f1427cfabbaa5cadba" have entirely different histories.
fae5c23722
...
392ba9a040
10 changed files with 407 additions and 1384 deletions
|
|
@ -26,19 +26,12 @@ Wakdo est une **borne de commande tactile** pour un restaurant de restauration r
|
||||||
|
|
||||||
### Acteurs
|
### Acteurs
|
||||||
|
|
||||||
| Acteur | Role RBAC | Interface |
|
| Acteur | Role | Interface |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Client** | (non authentifie) | Borne tactile (Bloc 1, canal `kiosk`) |
|
| **Client** | Passe sa commande sur la borne | Borne tactile (Bloc 1) |
|
||||||
| **Counter** | `counter` | Back-office : saisit les commandes au **comptoir**, les remet au client, peut annuler |
|
| **Accueil** | Saisit commandes au **comptoir** (client au guichet) ou au **drive** (client en voiture via intercom + casque equipier), remet les commandes livrees aux clients | Back-office (Bloc 2) |
|
||||||
| **Drive** | `drive` | Back-office : saisit les commandes au **drive** (intercom + casque), les remet, peut annuler |
|
| **Preparation** | Voit les commandes a preparer triees par heure croissante, les declare "preparees" | Back-office (Bloc 2) |
|
||||||
| **Kitchen** | `kitchen` | Back-office : voit la file des commandes `paid` triees par `paid_at` croissant, en **lecture seule** (KDS visuel, aucune transition) |
|
| **Administration** | CRUD sur donnees (produits, menus, prix, images) + gestion utilisateurs + stats | Back-office (Bloc 2) |
|
||||||
| **Manager** | `manager` | Back-office : catalogue (create/update), stock/reappro, statistiques |
|
|
||||||
| **Administration** | `admin` | Back-office : catalogue complet (+ suppressions), gestion utilisateurs, roles et permissions (RBAC), stats |
|
|
||||||
|
|
||||||
> Modele v0.2 : 5 roles RBAC (`admin`, `manager`, `kitchen`, `counter`, `drive`)
|
|
||||||
> + Customer non authentifie. RBAC permission-driven (le code teste une
|
|
||||||
> permission, pas un nom de role) ; catalogue de 23 permissions fige au seed.
|
|
||||||
> Voir `docs/merise/dictionary.md` 3.15-3.18 et `docs/uml/use-cases.md`.
|
|
||||||
|
|
||||||
### Processus metier cle
|
### Processus metier cle
|
||||||
|
|
||||||
|
|
@ -53,21 +46,21 @@ Client Borne (Bloc 1) API (Bloc 2) BDD
|
||||||
│ │─POST /api/orders─────▶│───INSERT──────────▶│
|
│ │─POST /api/orders─────▶│───INSERT──────────▶│
|
||||||
│ │◀──────────201─────────│ │
|
│ │◀──────────201─────────│ │
|
||||||
│─recupere au comptoir │ │ │
|
│─recupere au comptoir │ │ │
|
||||||
Kitchen voit la file des commandes paid (lecture seule, KDS)
|
Preparation voit commande pending
|
||||||
Counter / Drive remettent au client
|
→ declare "preparee"
|
||||||
→ declarent "livree" (geste unique paid -> delivered)
|
Accueil voit commande prete
|
||||||
|
→ declare "livree"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Regles metier (MCT - a modeliser en Merise)
|
### Regles metier (MCT - a modeliser en Merise)
|
||||||
|
|
||||||
- Un **menu** = burger fixe + slots a choix (boisson, accompagnement, sauce). Modele relationnel `menu_slot` + `menu_slot_option` (voir `dictionary.md` 3.4-3.5)
|
- Un **menu** = burger + accompagnement (frites OU salade) + boisson + sauce
|
||||||
- Format **Normal / Maxi** au niveau du menu (deux prix : `price_normal_cents`, `price_maxi_cents`) ; le Maxi agrandit accompagnement + boisson uniquement
|
- Les **accompagnements** et **boissons** ont **2 tailles** (normale / grande)
|
||||||
- **Personnalisation des ingredients** (retirer = gratuit, ajouter = supplement) sur les sandwichs composes, via le configurateur (`ingredient`, `product_ingredient`, `order_item_modifier`)
|
- **Grande taille** = +0,50 € sur le prix de base
|
||||||
- **TVA portee par le produit** (`vat_rate` : 10% defaut, 5,5% contenant conservable), calculee ligne par ligne et snapshotee sur `order_item` (fact-check BOFiP, voir `dictionary.md` note 9)
|
- Une **commande** a un **numero** saisi par le client (remplace le paiement dans le cadre de l'exam)
|
||||||
- Une **commande** a un **numero** saisi par le client, prefixe par canal `K`/`C`/`D` (remplace le paiement dans le cadre de l'exam)
|
- Statuts commande : `pending` -> `preparing` -> `ready` -> `delivered` (ou `cancelled`)
|
||||||
- Statuts commande (machine a **4 etats**) : `pending_payment` -> `paid` -> `delivered` (+ `cancelled`). La transition `pending_payment -> paid` est **atomique** a la creation (saisie du numero = substitut de paiement). `cancelled` est atteignable depuis `pending_payment` et `paid` (pas depuis `delivered`). Plus de `preparing` / `ready` : la cuisine est en lecture seule, la remise est un geste unique
|
|
||||||
- **Source commande** (trace sur chaque commande) : `kiosk` (borne autonome) | `counter` (comptoir) | `drive` (drive-thru)
|
- **Source commande** (trace sur chaque commande) : `kiosk` (borne autonome) | `counter` (comptoir) | `drive` (drive-thru)
|
||||||
- Le canal de prepa (`kitchen`/`counter`/`drive`) voit la file des commandes `paid` triee par `paid_at` **croissant**, filtree par `role_visible_source` (kitchen voit tout ; counter voit kiosk+counter ; drive voit drive)
|
- La preparation voit les commandes triees par **heure de livraison croissante** (tous canaux confondus)
|
||||||
- **Horaires service** : 10h00 → 01h00 du matin (service continu 15h, pas de fermeture intermediaire)
|
- **Horaires service** : 10h00 → 01h00 du matin (service continu 15h, pas de fermeture intermediaire)
|
||||||
- **Pas de notion de "session de service" a modeliser** : les equipiers se relaient, chacun se connecte a sa prise de poste et se deconnecte a la fin. Pas de "shift" a tracer dans la BDD (hors scope RNCP)
|
- **Pas de notion de "session de service" a modeliser** : les equipiers se relaient, chacun se connecte a sa prise de poste et se deconnecte a la fin. Pas de "shift" a tracer dans la BDD (hors scope RNCP)
|
||||||
- **Fenetre de maintenance systeme** : 01h30 → 09h30 (crons lourds, backups, agregations) — evite toute interference avec le service actif
|
- **Fenetre de maintenance systeme** : 01h30 → 09h30 (crons lourds, backups, agregations) — evite toute interference avec le service actif
|
||||||
|
|
@ -194,8 +187,8 @@ Reseaux :
|
||||||
| TLS | Let's Encrypt via Traefik | auto | `acme.json` existant |
|
| TLS | Let's Encrypt via Traefik | auto | `acme.json` existant |
|
||||||
| Conteneurisation | Docker + docker compose | v2 | Cr 7.c |
|
| Conteneurisation | Docker + docker compose | v2 | Cr 7.c |
|
||||||
| Orchestration locale | Makefile | — | Cr 7.b (script) + Cr 7.c.4 (une commande) |
|
| Orchestration locale | Makefile | — | Cr 7.b (script) + Cr 7.c.4 (une commande) |
|
||||||
| CI/CD | Forgejo Actions (act_runner auto-heberge) | — | Cr 7.d |
|
| CI/CD | GitHub Actions | — | Cr 7.d |
|
||||||
| Versioning | Git + Forgejo auto-heberge (push-mirror GitHub) | — | Cr 4.f (collaboration) |
|
| Versioning | Git + GitHub | — | Cr 4.f (collaboration) |
|
||||||
| Hooks Git | pre-commit + commit-msg | versionnes dans `.githooks/` | Conventional Commits |
|
| Hooks Git | pre-commit + commit-msg | versionnes dans `.githooks/` | Conventional Commits |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -227,11 +220,10 @@ Reseaux :
|
||||||
|
|
||||||
**IN scope — Back-office :**
|
**IN scope — Back-office :**
|
||||||
- Authentification sessions securisees (hash bcrypt/argon2, protection CSRF, fixation session) — duree de session adaptee a un poste complet d'equipier (idle timeout 4h, absolute timeout 10h)
|
- Authentification sessions securisees (hash bcrypt/argon2, protection CSRF, fixation session) — duree de session adaptee a un poste complet d'equipier (idle timeout 4h, absolute timeout 10h)
|
||||||
- 5 roles RBAC seed : `admin`, `manager`, `kitchen`, `counter`, `drive` (RBAC permission-driven, 23 permissions figees au seed ; roles personnalises possibles)
|
- 3 roles RBAC : `admin`, `preparation`, `accueil`
|
||||||
- **Admin** : CRUD complet catalogue (+ suppressions), gestion utilisateurs, roles et permissions (RBAC), stats
|
- **Admin** : CRUD categories, produits (nom, description, prix, image, dispo), menus (composition + options), utilisateurs
|
||||||
- **Manager** : catalogue (create/update), stock (reappro + inventaire), statistiques ; pas d'acces utilisateurs ni RBAC
|
- **Preparation** : liste commandes a preparer triees par heure livraison croissante, bouton "declarer preparee"
|
||||||
- **Kitchen** : file des commandes `paid` triee par `paid_at` croissant, en **lecture seule** (KDS visuel) ; inventaire
|
- **Accueil** : saisir commande manuellement (comptoir ou drive-thru via casque/intercom), bouton "declarer livree" ; champ `source` enregistre sur chaque commande (`counter` ou `drive`)
|
||||||
- **Counter** / **Drive** : saisir une commande (comptoir / drive-thru via casque/intercom), bouton "declarer livree" (geste unique `paid -> delivered`), annuler ; `source` auto-tague depuis `role.order_source` ; inventaire
|
|
||||||
- Upload images produits (validation type MIME + taille + stockage dans volume `wakdo_uploads`)
|
- Upload images produits (validation type MIME + taille + stockage dans volume `wakdo_uploads`)
|
||||||
- Historique commandes par statut
|
- Historique commandes par statut
|
||||||
- Stats de base (commandes du jour, CA jour, produits top)
|
- Stats de base (commandes du jour, CA jour, produits top)
|
||||||
|
|
@ -269,10 +261,9 @@ Reseaux :
|
||||||
- `0 3 * * *` — backup BDD quotidien a 03h00 (entre fin service 01h et ouverture 10h)
|
- `0 3 * * *` — backup BDD quotidien a 03h00 (entre fin service 01h et ouverture 10h)
|
||||||
- `*/15 * * * *` — purge sessions expirees toutes les 15 min (leger, peut tourner en service)
|
- `*/15 * * * *` — purge sessions expirees toutes les 15 min (leger, peut tourner en service)
|
||||||
- `30 4 * * *` — agregation stats commandes a 04h30 sur le **jour de service** ecoule (10h J-1 → 01h J)
|
- `30 4 * * *` — agregation stats commandes a 04h30 sur le **jour de service** ecoule (10h J-1 → 01h J)
|
||||||
- **CI Forgejo Actions** (act_runner auto-heberge) : lint PHP + PHPStan + PHPUnit + secret-scan (gitleaks) sur PR -> dev
|
- **CI GitHub Actions** : lint PHP + PHPUnit sur PR -> dev
|
||||||
- **CD Forgejo Actions** : deploy auto sur merge main (SSH + pull + `make rebuild`)
|
- **CD GitHub Actions** : deploy auto sur merge main (SSH + pull + `make rebuild`)
|
||||||
- `.env.example` documente (parametres securite : argon2id, lockout, seuils throttle, retention RGPD), secrets hors du repo
|
- `.env.example` documente, secrets hors du repo
|
||||||
- `php.ini` durci (expose_php off, session cookies httponly/secure/samesite, upload limite)
|
|
||||||
- Healthcheck Traefik + readiness probes
|
- Healthcheck Traefik + readiness probes
|
||||||
- Logs centralises (stdout des conteneurs)
|
- Logs centralises (stdout des conteneurs)
|
||||||
- Documentation deploiement + architecture (schemas dans `docs/`)
|
- Documentation deploiement + architecture (schemas dans `docs/`)
|
||||||
|
|
@ -336,8 +327,8 @@ Reseaux :
|
||||||
| Cr 7.c.3 | App conteneurisee complete | 4 services (web, app, db, cron) |
|
| Cr 7.c.3 | App conteneurisee complete | 4 services (web, app, db, cron) |
|
||||||
| Cr 7.c.4 | **Une ligne de commande** | `make init` lance toute la stack + migrate + seed |
|
| Cr 7.c.4 | **Une ligne de commande** | `make init` lance toute la stack + migrate + seed |
|
||||||
| Cr 7.d.1 | Architecture serveur | Traefik reverse + reseaux segmentes documentes |
|
| Cr 7.d.1 | Architecture serveur | Traefik reverse + reseaux segmentes documentes |
|
||||||
| Cr 7.d.2 | Tests avant deploy | CI PHPUnit + PHPStan + secret-scan sur PR (Forgejo Actions) |
|
| Cr 7.d.2 | Tests avant deploy | CI PHPUnit + lint sur PR |
|
||||||
| Cr 7.d.3 | Integration/deploiement continus | Forgejo Actions deploy automatique sur merge main |
|
| Cr 7.d.3 | Integration/deploiement continus | GitHub Actions deploy automatique sur merge main |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -353,13 +344,13 @@ main ← production (tag vX.Y.Z sur chaque release)
|
||||||
fix/* ← corrections
|
fix/* ← corrections
|
||||||
refactor/* ← refactos
|
refactor/* ← refactos
|
||||||
docs/* ← doc seulement
|
docs/* ← doc seulement
|
||||||
ci/* ← Forgejo Actions
|
ci/* ← GitHub Actions
|
||||||
db/* ← migrations / schema BDD
|
db/* ← migrations / schema BDD
|
||||||
chore/* ← tooling, config
|
chore/* ← tooling, config
|
||||||
test/* ← ajout de tests
|
test/* ← ajout de tests
|
||||||
```
|
```
|
||||||
|
|
||||||
Les branches `main` et `dev` sont **protegees** cote Forgejo (push direct interdit, force-push bloque, PR obligatoire via l'API `branch_protections`). Hook pre-commit local les bloque egalement.
|
Les branches `main` et `dev` sont **protegees** cote GitHub. Pas de commit direct autorise. Hook pre-commit local les bloque egalement.
|
||||||
|
|
||||||
**Flow :**
|
**Flow :**
|
||||||
1. `git checkout -b feat/menu-composition` (depuis `dev`)
|
1. `git checkout -b feat/menu-composition` (depuis `dev`)
|
||||||
|
|
@ -441,10 +432,9 @@ Les branches `main` et `dev` sont **protegees** cote Forgejo (push direct interd
|
||||||
| 10 | Service cron dedie | Cr 7.b.3 explicite + realiste prod |
|
| 10 | Service cron dedie | Cr 7.b.3 explicite + realiste prod |
|
||||||
| 11 | Makefile avec `make init` | Cr 7.c.4 + demonstration DevOps |
|
| 11 | Makefile avec `make init` | Cr 7.c.4 + demonstration DevOps |
|
||||||
| 12 | Conventional Commits + hooks | Cr 4.f.x + discipline de versioning |
|
| 12 | Conventional Commits + hooks | Cr 4.f.x + discipline de versioning |
|
||||||
| 13 | Branches feat/* -> dev -> main | Pipeline propre pour jury, PR tracee (Forgejo, mirror GitHub) |
|
| 13 | Branches feat/* -> dev -> main | Pipeline propre pour jury, GitHub PR trace |
|
||||||
| 14 | CI/CD Forgejo Actions (act_runner auto-heberge) | Cr 7.d explicite ; forge + CI maitrisees de bout en bout (argument Bloc 5) |
|
| 14 | CI/CD GitHub Actions | Cr 7.d explicite dans referentiel |
|
||||||
| 15 | RGPD implemente minimal | Cr 3.d.1-4 evaluees meme projet ecole |
|
| 15 | RGPD implemente minimal | Cr 3.d.1-4 evaluees meme projet ecole |
|
||||||
| 16 | Security-by-design (threat model STRIDE + classification donnees) | Audit Cr 7.a ; stock en %, throttle brute-force, retention RGPD documentes en amont du code |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -452,18 +442,18 @@ Les branches `main` et `dev` sont **protegees** cote Forgejo (push direct interd
|
||||||
|
|
||||||
| Phase | Scope | Budget (h) | Deadline intermediaire |
|
| Phase | Scope | Budget (h) | Deadline intermediaire |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **P0 - Setup** | PC, arborescence, Docker, hooks, CI squelette, migration Forgejo + act_runner | 22 | Semaine 1 |
|
| **P0 - Setup** | PC, arborescence, Docker, hooks, CI squelette, init Git/GitHub | 20 | Semaine 1 |
|
||||||
| **P1 - Conception Merise + Security-by-design** | Dictionnaire, MCD, MCT, MLD, schemas fonctionnels, DDL, threat model STRIDE + classification donnees + sequence securite | 38 | Semaine 3 |
|
| **P1 - Conception Merise** | Dictionnaire, MCD, MCT, MLD, schemas fonctionnels, DDL | 30 | Semaine 3 |
|
||||||
| **P2 - Back squelette** | POO base (Core, Router, Autoloader, DB), auth + roles | 30 | Semaine 6 |
|
| **P2 - Back squelette** | POO base (Core, Router, Autoloader, DB), auth + roles | 30 | Semaine 6 |
|
||||||
| **P3 - Back CRUD admin** | Produits, menus, utilisateurs, views | 40 | Semaine 10 |
|
| **P3 - Back CRUD admin** | Produits, menus, utilisateurs, views | 40 | Semaine 10 |
|
||||||
| **P4 - API REST** | Endpoints + CORS + tests | 20 | Semaine 12 |
|
| **P4 - API REST** | Endpoints + CORS + tests | 20 | Semaine 12 |
|
||||||
| **P5 - Front borne** | Integration maquette, Ajax, accessibilite, responsive | 60 | Semaine 16 |
|
| **P5 - Front borne** | Integration maquette, Ajax, accessibilite, responsive | 60 | Semaine 16 |
|
||||||
| **P6 - Tests + finition** | PHPUnit, tests E2E borne, corrections | 25 | Semaine 18 |
|
| **P6 - Tests + finition** | PHPUnit, tests E2E borne, corrections | 25 | Semaine 18 |
|
||||||
| **P7 - DevOps finalisation** | Forgejo Actions CI/CD (PHPUnit + PHPStan + secret-scan + deploy auto), crons, SECURITY.md, docs argumentation | 22 | Semaine 19 |
|
| **P7 - DevOps finalisation** | CI/CD deploy auto, crons, docs argumentation | 20 | Semaine 19 |
|
||||||
| **P8 - Prep soutenance** | README pour jury, schemas finaux, repetitions, modifs en direct | 15 | Semaine 20 |
|
| **P8 - Prep soutenance** | README pour jury, schemas finaux, repetitions, modifs en direct | 15 | Semaine 20 |
|
||||||
| **TOTAL** | | **272** | **Semaine 20 = fin aout 2026** |
|
| **TOTAL** | | **260** | **Semaine 20 = fin aout 2026** |
|
||||||
|
|
||||||
Buffer : ~8 h pour imprevus. Cible effective : ~264 h sur 20 semaines = **~13 h/semaine**.
|
Buffer : ~20 h pour imprevus. Cible effective : ~240 h sur 20 semaines = **12 h/semaine**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -491,7 +481,7 @@ Buffer : ~8 h pour imprevus. Cible effective : ~264 h sur 20 semaines = **~13 h/
|
||||||
- `docker-compose.yml` commente
|
- `docker-compose.yml` commente
|
||||||
- Dockerfiles customs commentes
|
- Dockerfiles customs commentes
|
||||||
- `Makefile` avec `make help`
|
- `Makefile` avec `make help`
|
||||||
- `.forgejo/workflows/` avec CI (PHPUnit + PHPStan + secret-scan) + CD
|
- `.github/workflows/` avec CI + CD
|
||||||
- Crontab documente
|
- Crontab documente
|
||||||
- Script de backup/restore teste
|
- Script de backup/restore teste
|
||||||
- Architecture serveur decrite (`docs/architecture/deployment.md`)
|
- Architecture serveur decrite (`docs/architecture/deployment.md`)
|
||||||
|
|
@ -685,138 +675,4 @@ Ces regles tiennent lieu de garde-fous pendant toute la duree du projet. Les enf
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 19. Security threat model and data classification
|
*Document vivant — version 1.1 — 2026-04-24 (ajout section 17 transparence IA). A mettre a jour a chaque decision structurante.*
|
||||||
|
|
||||||
Cette section formalise la couche **security-by-design** ajoutee au modele Merise v0.2
|
|
||||||
(voir `docs/merise/dictionary.md` note 13, `docs/merise/mlt.md` section 2 pour les regles
|
|
||||||
transverses RG-T13 a RG-T21). Elle se lit a deux niveaux : un **registre des risques** de
|
|
||||||
synthese pour une lecture gestion, suivi d'une **analyse STRIDE par element** pour la
|
|
||||||
profondeur technique, puis une **matrice de classification des donnees** en 4 niveaux. Tous
|
|
||||||
les claims securite sont rattaches a un mecanisme concret (une regle RG-T, une colonne, une
|
|
||||||
entite) plutot qu'enonces comme des absolus.
|
|
||||||
|
|
||||||
### 19.1 Perimetre et frontieres de confiance
|
|
||||||
|
|
||||||
Le systeme expose cinq frontieres de confiance (trust boundaries), correspondant aux points
|
|
||||||
d'entree analyses ci-dessous :
|
|
||||||
|
|
||||||
- **E1 — Borne kiosk (public anonyme)** : `POST /api/orders`, consultation du catalogue.
|
|
||||||
Aucune authentification ; la borne est anonyme par conception (les commandes kiosk ont
|
|
||||||
`customer_order.acting_user_id = NULL`). Surface la plus exposee, donc traitee sans
|
|
||||||
hypothese de confiance sur l'entree.
|
|
||||||
- **E2 — Back-office admin (staff authentifie, poste partage + PIN par equipier)** : CRUD
|
|
||||||
catalogue/menus/ingredients, RBAC, gestion utilisateurs, stock, annulation de commande,
|
|
||||||
stats. Session partagee par poste pour le flux courant ; un PIN par equipier
|
|
||||||
(`user.pin_hash`) re-autorise l'ensemble sensible (RG-T13).
|
|
||||||
- **E3 — Surface d'authentification** : login (`AUTHENTICATE_USER`, op 25, `mlt.md` 12.1) et
|
|
||||||
reinitialisation de mot de passe (`RESET_PASSWORD`, op 28, `mlt.md` 12.3).
|
|
||||||
- **E4 — Couche donnees / BDD** : acces PDO, requetes preparees (RG-T06), allowlists
|
|
||||||
(RG-T16/RG-T17), integrite transactionnelle (RG-T08/RG-T11), snapshots immuables (RG-T05).
|
|
||||||
- **E5 — Stock / inventaire** : decrement de vente, reappro, comptage d'inventaire, avec un
|
|
||||||
journal append-only `stock_movement` et attribution de l'acteur ; la correction d'inventaire
|
|
||||||
est PIN-gated (RG-T13, `mlt.md` 9.2) car elle peut masquer de la demarque (shrinkage).
|
|
||||||
|
|
||||||
Hors perimetre de cette section : la securite reseau/infra (Traefik, segmentation Docker,
|
|
||||||
TLS), couverte en section 5 ; le durcissement CORS, couvert en section 5.
|
|
||||||
|
|
||||||
### 19.2 Registre des risques (risk register)
|
|
||||||
|
|
||||||
Synthese gestion. Likelihood et residual risk sont evalues a dire d'expert pour ce projet
|
|
||||||
fictif (`[REASONING]`, non quantifies par benchmark) ; chaque mitigation cite une regle RG-T
|
|
||||||
et/ou une entite reelle du modele.
|
|
||||||
|
|
||||||
| # | Actif | Menace | Impact | Likelihood | Mitigation (regle / entite) | Risque residuel |
|
|
||||||
|---|---|---|---|---|---|---|
|
|
||||||
| R1 | Recette (cash) sur commande payee | Un equipier annule une commande `paid` pour detourner l'encaissement (fraude interne) | Fort | Moyenne | `CANCEL_ORDER` (`mlt.md` 7.1) PIN-gated (RG-T13) + ecriture `audit_log` dans la meme transaction (RG-T14, RG-T11) ; acteur capture via `audit_log.actor_user_id` | Faible — l'annulation reste possible mais devient nominative et tracee ; dissuasion plus que blocage |
|
|
||||||
| R2 | `product.price_cents` / `vat_rate` / `role_id` | Falsification via un champ de formulaire injecte (mass-assignment) | Fort | Moyenne | Allowlist de colonnes par operation (RG-T16) sur `UPDATE_PRODUCT` (`mlt.md` 8.2) et `UPDATE_USER` (10.2) ; seules les colonnes autorisees sont bindees | Faible — les champs hors allowlist sont ignores ; un changement de prix reste audite (RG-T14) |
|
|
||||||
| R3 | Comptes back-office (`user.password_hash`) | Brute-force sur le login staff | Moyen | Haute | Backoff degressif par compte (`user.failed_login_attempts` / `lockout_until`) + par IP (`login_throttle`, entite 21) ; gate avant verification (`mlt.md` 12.1 PRE-3, RG-8) | Faible — ralentissement sans lock indefini ; un service de 15h n'est pas bloque par une saisie maladroite |
|
|
||||||
| R4 | Vues kiosk et admin (texte stocke) | XSS stocke via `product.name` / `ingredient.name` / `user.first_name` | Moyen | Moyenne | Echappement au rendu (RG-T15) : `htmlspecialchars(..., ENT_QUOTES)` cote admin, injection via `textContent` (pas `innerHTML`) cote kiosk vanilla-JS | Faible — l'echappement reduit le risque d'execution de script injecte |
|
|
||||||
| R5 | `ingredient.stock_quantity` | Survente (oversell) sous concurrence multi-borne | Moyen | Moyenne | Decrement atomique auto-verrouillant (RG-T20) sans read-gate + disponibilite calculee (RG-T21) ; `stock_quantity` signe, la magnitude de survente est remontee aux managers | Moyen accepte — le systeme ne bloque pas une commande sur le stock ; la survente est mesuree, pas empechee (decision metier) |
|
|
||||||
| R6 | Commande payee | Double-charge sur retry reseau de `POST /api/orders` | Moyen | Moyenne | Idempotence (RG-T19) : `customer_order.idempotency_key` UNIQUE ; un retry renvoie la commande existante au lieu d'en creer une seconde | Faible — la cle UNIQUE deduplique les rejeux ; depend d'une cle client correctement generee |
|
|
||||||
| R7 | PII utilisateur (`user.email`/`first_name`/`last_name`) | Demande d'effacement RGPD non honoree, ou rupture de l'integrite referentielle a la suppression | Fort (conformite) | Faible | Anonymisation (`ERASE_USER_PII`, `mlt.md` 10.5) : la ligne est conservee, PII remplacees par un placeholder `anon-<id>@wakdo.invalid`, credentials invalides, `anonymized_at` pose ; `audit_log` retient sa propre fenetre | Faible — effacement et tracabilite coexistent ; les FK (`audit_log.actor_user_id`, `customer_order.acting_user_id`, `stock_movement.user_id`) restent valides |
|
|
||||||
| R8 | Matrice RBAC (`role_permission`) | Elevation de privilege via modification de role non controlee | Fort | Faible | `MANAGE_RBAC` (`mlt.md` 10.4) PIN-gated (RG-T13) + `audit_log` du diff de permissions (RG-T14, RG-6) ; `role_id` derriere l'allowlist (RG-T16) | Faible — tout gain/perte de capacite est nominatif et trace |
|
|
||||||
| R9 | `stock_movement` (demarque) | Correction d'inventaire masquant une demarque | Moyen | Moyenne | `INVENTORY_COUNT` (`mlt.md` 9.2) PIN-gated (RG-T13) ; le `user_id` capture par PIN est ecrit dans `stock_movement.user_id` (append-only) | Faible — la correction devient attribuable a une personne meme sur poste partage |
|
|
||||||
|
|
||||||
### 19.3 Analyse STRIDE par element
|
|
||||||
|
|
||||||
Un bloc par categorie STRIDE, mappe aux controles reels du modele (verifies contre
|
|
||||||
`mlt.md` section 2).
|
|
||||||
|
|
||||||
**Spoofing (usurpation d'identite).** L'authentification back-office repose sur argon2id
|
|
||||||
(`user.password_hash`, `mlt.md` 12.1 RG-2) avec regeneration de session a la connexion
|
|
||||||
(`session_regenerate(true)`, RG-3) pour contrer la fixation. Le login est enumeration-safe :
|
|
||||||
meme erreur generique que l'email existe ou non, avec un `password_verify` leurre pour garder
|
|
||||||
le timing comparable (RG-2). Sur un poste partage, un PIN par equipier (`user.pin_hash`,
|
|
||||||
RG-T13) re-authentifie l'acteur reel pour les actions sensibles. La reinitialisation de mot de
|
|
||||||
passe (`RESET_PASSWORD`, `mlt.md` 12.3) stocke le token hashe (`password_reset_token_hash`),
|
|
||||||
n'envoie le token brut qu'une seule fois, l'expire a 1h et le rend a usage unique (RG-2/RG-3) ;
|
|
||||||
la phase requete renvoie une reponse neutre identique que le compte existe ou non (RG-1,
|
|
||||||
enumeration-safe). La borne kiosk est anonyme
|
|
||||||
par conception, donc hors perimetre d'usurpation (pas de compte a usurper).
|
|
||||||
|
|
||||||
**Tampering (alteration).** L'allowlist de mass-assignment (RG-T16) limite les colonnes
|
|
||||||
bindees aux champs autorises par operation, protegeant `price_cents`, `vat_rate`, `role_id`,
|
|
||||||
`is_active`, `status`. La validation cote serveur (RG-T18) re-verifie type, plage, longueur,
|
|
||||||
appartenance ENUM et existence des FK independamment du client. Les requetes preparees PDO
|
|
||||||
(RG-T06) traitent les valeurs hors de la chaine SQL, ce qui ferme l'injection SQL par valeur.
|
|
||||||
Les identifiants SQL dynamiques (colonne et direction d'un `ORDER BY`/`GROUP BY`) sont resolus
|
|
||||||
contre une allowlist fixe avant construction de la requete (RG-T17), car un identifiant ne
|
|
||||||
peut pas etre bind comme une valeur.
|
|
||||||
Les snapshots de commande (`order_item.label_snapshot`, `unit_price_cents_snapshot`,
|
|
||||||
`vat_rate_snapshot`) sont immuables apres INSERT (RG-T05), preservant l'integrite historique
|
|
||||||
des commandes placees. La re-validation serveur des modifiers (`mlt.md` 3.3 RG-9) rejette un
|
|
||||||
`POST` forge ajoutant un ingredient non-`is_addable`.
|
|
||||||
|
|
||||||
**Repudiation (deni d'action).** Le journal `audit_log` (entite 20, RG-T14) enregistre les
|
|
||||||
actions sensibles non-stock avec `actor_user_id` (capture par PIN, RG-T13), `actor_role_id`
|
|
||||||
(denormalise pour survivre a l'anonymisation), `action_code`, `entity_type`/`entity_id` et un
|
|
||||||
`summary` non-personnel ; pas d'UPDATE/DELETE applicatif. L'attribution des commandes
|
|
||||||
comptoir/drive passe par `customer_order.acting_user_id` (`mlt.md` 4.1 RG-5) et celle du stock
|
|
||||||
par `stock_movement.user_id` (`mlt.md` 9.1/9.2). Les actions stock ne sont pas doublement
|
|
||||||
journalisees : `stock_movement` (append-only) fournit deja la piste.
|
|
||||||
|
|
||||||
**Information disclosure (divulgation).** La matrice de classification (19.4) borne ce qui
|
|
||||||
sort des logs et des reponses API. Les erreurs d'auth sont generiques (RG-2, pas de
|
|
||||||
distinction email inconnu / mot de passe faux). L'`audit_log` stocke des **noms de champs**
|
|
||||||
modifies, pas les valeurs PII (`audit_log.details`, RG-T14). L'attribution de stock
|
|
||||||
(`stock_movement.user_id`) n'est visible que pour manager/admin ; le staff de ligne voit les
|
|
||||||
deltas sans l'identite de l'acteur (`mlt.md` 9.3 RG-4). Les credentials (`password_hash`,
|
|
||||||
`pin_hash`, `password_reset_token_hash`) sont tenus hors logs et hors reponses API.
|
|
||||||
|
|
||||||
**Denial of service.** Le throttling de login est degressif (backoff exponentiel plafonne)
|
|
||||||
plutot qu'un lock indefini, dans les deux dimensions compte (`user.lockout_until`) et IP
|
|
||||||
(`login_throttle.lockout_until`, `mlt.md` 12.1 RG-8) : une saisie maladroite ne bloque pas une
|
|
||||||
cuisine en plein service de 15h continu. L'idempotence (RG-T19) absorbe les doubles-soumissions
|
|
||||||
de retry reseau sur le kiosk anonyme. Le decrement de stock atomique (RG-T20) evite tout
|
|
||||||
contentieux de verrou (pas de `SELECT ... FOR UPDATE`, pas d'ordre de deadlock).
|
|
||||||
|
|
||||||
**Elevation of privilege.** Le RBAC est permission-driven : le code teste une permission, pas
|
|
||||||
un nom de role (catalogue de 23 permissions fige au seed, `dictionary.md` 3.17). Les
|
|
||||||
changements de role passent par `MANAGE_RBAC` (`mlt.md` 10.4) PIN-gated (RG-T13) et audites
|
|
||||||
avec le diff de permissions (RG-T14, RG-6). `role_id` est derriere l'allowlist de
|
|
||||||
mass-assignment (RG-T16) sur `UPDATE_USER` (10.2). Les permissions sont rechargees depuis la
|
|
||||||
BDD a chaque verification (`mlt.md` 10.4 RG-3), donc un changement de droits prend effet sans
|
|
||||||
re-login force.
|
|
||||||
|
|
||||||
### 19.4 Matrice de classification des donnees (4 niveaux)
|
|
||||||
|
|
||||||
Les 21 entites du modele (`dictionary.md` 3.1-3.21) sont reparties en quatre niveaux. La
|
|
||||||
classification suit l'entite ; quelques colonnes sont surclassees explicitement (credentials,
|
|
||||||
PII).
|
|
||||||
|
|
||||||
| Niveau | Definition | Entites / colonnes | Regle de manipulation |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **RESTRICTED** (secrets / credentials) | Secrets d'authentification ; tenus hors de toute exposition | Colonnes de `user` (14) : `password_hash`, `pin_hash`, `password_reset_token_hash` | Hors logs et hors reponses API ; argon2id ; invalides a l'anonymisation (`mlt.md` 10.5 RG-1) ; exclus de `audit_log.details` qui ne retient que des noms de champs (RG-T14) |
|
|
||||||
| **CONFIDENTIAL** (PII, RGPD) | Donnees a caractere personnel d'un staff identifiable | Colonnes de `user` (14) : `email`, `first_name`, `last_name` | Sujet a l'anonymisation a l'effacement (`ERASE_USER_PII`, op 27) ; `audit_log` stocke les noms de champs, pas les valeurs ; echappement au rendu (RG-T15) |
|
|
||||||
| **INTERNAL** (sensible metier) | Donnees d'exploitation, non publiques, a acces restreint par RBAC | `customer_order` (10), `order_item` (11), `order_item_selection` (12), `order_item_modifier` (13), `stock_movement` (19), `audit_log` (20), `login_throttle` (21, contient l'IP source), `role` (15), `permission` (17), `role_permission` (18), `role_visible_source` (16) ; sorties de stats (`READ_STATS`, op 24) | Acces filtre par permission (RG-T03) ; attribution stock visible manager/admin seulement (`mlt.md` 9.3 RG-4) ; integrite par snapshots (RG-T05) et transactions (RG-T08/RG-T11) |
|
|
||||||
| **PUBLIC** (catalogue, face kiosk) | Donnees servies a la borne anonyme | `category` (1), `product` (2), `menu` (3), `menu_slot` (4), `menu_slot_option` (5), `ingredient` (6, nom + dispo calculee), `product_ingredient` (7), `allergen` (8), `ingredient_allergen` (9) | Lecture publique via `LOAD_CATALOGUE` (op 1) ; ecriture reservee admin/manager (RG-T03) ; texte echappe au rendu (RG-T15) ; disponibilite calculee (RG-T21) |
|
|
||||||
|
|
||||||
**Couverture** : 21/21 entites classifiees (9 PUBLIC, 11 INTERNAL incluant les deux entites
|
|
||||||
security-by-design `audit_log` et `login_throttle`, plus `user` dont les colonnes sont
|
|
||||||
reparties entre RESTRICTED, CONFIDENTIAL et — pour `is_active`, `role_id`, `last_login_at`,
|
|
||||||
les compteurs de throttle — INTERNAL). L'entite `user` (14) est la seule a porter trois
|
|
||||||
niveaux simultanement, d'ou son traitement par colonne.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Document vivant — version 1.3 — 2026-06-15 (drift GitHub -> Forgejo Actions corrige, CI securite PHPStan/secret-scan, planning rechiffre pour la couche security-by-design). A mettre a jour a chaque decision structurante.*
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Data Dictionary — Wakdo
|
# Data Dictionary — Wakdo
|
||||||
|
|
||||||
**Merise phase** : P1 - Conception, step 1 (data dictionary first, mantra #33)
|
**Merise phase** : P1 - Conception, step 1 (data dictionary first, mantra #33)
|
||||||
**Version** : v0.2 — prod-like, 21 entities (19 prod-like + security-by-design layer, incl. the new `login_throttle` entity)
|
**Version** : v0.2 — prod-like, 19 entities
|
||||||
**Date** : 2026-06-04 (security-by-design additions 2026-06-11)
|
**Date** : 2026-06-04
|
||||||
**Branch** : `feat/p1-conception`
|
**Branch** : `feat/p1-conception`
|
||||||
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7); security-by-design layer in progress (see note 13)
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
**Author** : BYAN (methodology layer)
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -193,32 +193,21 @@ Elementary ingredient used in product composition. Carries stock data.
|
||||||
| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | |
|
| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | |
|
||||||
| `name` | VARCHAR(120) | NO | — | UNIQUE | e.g., "Sesame Bun", "Cheddar Slice", "Ketchup Portion" |
|
| `name` | VARCHAR(120) | NO | — | UNIQUE | e.g., "Sesame Bun", "Cheddar Slice", "Ketchup Portion" |
|
||||||
| `unit` | VARCHAR(40) | NO | — | — | packaging unit label: piece / portion / sachet 1kg / pot / bottle (free-form label, not an ENUM — units vary per ingredient) |
|
| `unit` | VARCHAR(40) | NO | — | — | packaging unit label: piece / portion / sachet 1kg / pot / bottle (free-form label, not an ENUM — units vary per ingredient) |
|
||||||
| `stock_quantity` | INT (signed) | NO | 0 | — | current stock in units. Signed INT with no `CHECK >= 0`: it MAY go negative when sales outrun counted stock (oversell magnitude, surfaced to managers). The system does not block an order on stock. |
|
| `stock_quantity` | INT | NO | 0 | CHECK >= 0 | current stock in units. Signed INT to allow negative detection (alert), but business rule enforces >= 0 |
|
||||||
| `stock_capacity` | INT | NO | — | CHECK > 0 | reference "full" level in units = the 100% used to compute the stock percentage. The `CHECK > 0` also guards the percentage division against divide-by-zero |
|
|
||||||
| `pack_size` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | units per restocking pack (e.g., 100 for a bag of 100 portions) |
|
| `pack_size` | SMALLINT UNSIGNED | NO | 1 | CHECK > 0 | units per restocking pack (e.g., 100 for a bag of 100 portions) |
|
||||||
| `pack_label` | VARCHAR(80) | YES | NULL | — | human label of the pack (e.g., "Sac 100 portions") |
|
| `pack_label` | VARCHAR(80) | YES | NULL | — | human label of the pack (e.g., "Sac 100 portions") |
|
||||||
| `low_stock_pct` | SMALLINT UNSIGNED | NO | 10 | CHECK BETWEEN 0 AND 100 | warning band, percent of capacity: `stock_quantity <= stock_capacity * low_stock_pct/100` triggers the low-stock indicator |
|
| `low_stock_threshold` | SMALLINT UNSIGNED | NO | 0 | CHECK >= 0 | alert threshold: stock_quantity <= this value triggers low-stock indicator |
|
||||||
| `critical_stock_pct` | SMALLINT UNSIGNED | NO | 5 | CHECK BETWEEN 0 AND 100 | auto-out-of-stock floor, percent of capacity: `stock_quantity <= stock_capacity * critical_stock_pct/100` makes the product computed out-of-stock |
|
|
||||||
| `is_active` | TINYINT(1) | NO | 1 | — | deactivate obsolete ingredients without deleting |
|
| `is_active` | TINYINT(1) | NO | 1 | — | deactivate obsolete ingredients without deleting |
|
||||||
| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit |
|
| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit |
|
||||||
| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit |
|
| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit |
|
||||||
|
|
||||||
**Table-level CHECK**: `critical_stock_pct < low_stock_pct` (the critical floor sits below the warning band).
|
|
||||||
|
|
||||||
**Stock decrement rule**: at the `paid` transition, each ingredient is decremented by
|
**Stock decrement rule**: at the `paid` transition, each ingredient is decremented by
|
||||||
`product_ingredient.quantity_normal` or `quantity_maxi` (selected by `order_item.format`)
|
`product_ingredient.quantity_normal` or `quantity_maxi` (selected by `order_item.format`)
|
||||||
multiplied by `order_item.quantity`, then adjusted by `order_item_modifier` rows. See note 7.
|
multiplied by `order_item.quantity`, then adjusted by `order_item_modifier` rows. See note 7.
|
||||||
**Restocking rule**: `stock_quantity += N * pack_size` (restocked in full packs).
|
**Restocking rule**: `stock_quantity += N * pack_size` (restocked in full packs).
|
||||||
**Cancellation rule**: stock is re-credited when a `paid` order is cancelled.
|
**Cancellation rule**: stock is re-credited when a `paid` order is cancelled.
|
||||||
**Stock model (percentage-based, three bands)**: the absolute alert threshold is replaced by a
|
**Low-stock alert**: computed at display time (`stock_quantity <= low_stock_threshold`);
|
||||||
percentage model anchored on `stock_capacity` (the 100% reference). The stock percentage is
|
no additional stored column.
|
||||||
computed, not stored: `stock_pct = ROUND(stock_quantity / stock_capacity * 100)`. The
|
|
||||||
`CHECK > 0` on `stock_capacity` guards this division against divide-by-zero. Three bands:
|
|
||||||
- **Normal** — above the low band: nothing flagged.
|
|
||||||
- **Low** — `stock_quantity <= stock_capacity * low_stock_pct/100`: orderable + manager alert.
|
|
||||||
The manager either pulls the product via `product.is_available=0`, or restocks to clear the alert.
|
|
||||||
- **Critical** — `stock_quantity <= stock_capacity * critical_stock_pct/100`: the product
|
|
||||||
auto-goes out-of-stock (computed availability, see rule RG-T21 in `mlt.md`); no extra stored column.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -282,9 +271,7 @@ Customer transaction: 1 order = 1 validated cart at a point in time.
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | |
|
| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | |
|
||||||
| `order_number` | VARCHAR(20) | NO | — | UNIQUE | human-readable format: `K`/`C`/`D`-YYYY-MM-DD-NNN. Prefix by channel: K=kiosk, C=counter, D=drive. See note 4. |
|
| `order_number` | VARCHAR(20) | NO | — | UNIQUE | human-readable format: `K`/`C`/`D`-YYYY-MM-DD-NNN. Prefix by channel: K=kiosk, C=counter, D=drive. See note 4. |
|
||||||
| `idempotency_key` | VARCHAR(36) | YES | NULL | UNIQUE | client-generated UUID to deduplicate a retried `POST /api/orders` (anti-double-charge). UNIQUE rejects duplicates; multiple NULLs allowed. Security-by-design, see note 13 |
|
|
||||||
| `source` | ENUM('kiosk','counter','drive') | NO | — | INDEX | input channel (who entered the order). Values in English, see note 5. |
|
| `source` | ENUM('kiosk','counter','drive') | NO | — | INDEX | input channel (who entered the order). Values in English, see note 5. |
|
||||||
| `acting_user_id` | INT UNSIGNED | YES | NULL | FK -> `user(id)`, ON DELETE SET NULL | back-office staff (counter/drive) who created the order, captured under PIN. NULL for `kiosk` (anonymous). Targeted accountability without forcing per-person login on the kiosk. See note 13 |
|
|
||||||
| `service_mode` | ENUM('dine_in','takeaway','drive') | NO | — | — | consumption mode, retained for stats/KPI only. No fiscal role (see note 9). `drive` source implies `drive` service_mode (cross-constraint enforced at app layer). |
|
| `service_mode` | ENUM('dine_in','takeaway','drive') | NO | — | — | consumption mode, retained for stats/KPI only. No fiscal role (see note 9). `drive` source implies `drive` service_mode (cross-constraint enforced at app layer). |
|
||||||
| `status` | ENUM('pending_payment','paid','delivered','cancelled') | NO | 'pending_payment' | INDEX | 4-state machine: `pending_payment -> paid -> delivered` (+ `cancelled`). See note 6. |
|
| `status` | ENUM('pending_payment','paid','delivered','cancelled') | NO | 'pending_payment' | INDEX | 4-state machine: `pending_payment -> paid -> delivered` (+ `cancelled`). See note 6. |
|
||||||
| `total_ht_cents` | INT UNSIGNED | NO | — | CHECK >= 0 | ex-VAT total, snapshot at order validation |
|
| `total_ht_cents` | INT UNSIGNED | NO | — | CHECK >= 0 | ex-VAT total, snapshot at order validation |
|
||||||
|
|
@ -397,13 +384,6 @@ are not authenticated and have no row here.
|
||||||
| `role_id` | INT UNSIGNED | NO | — | FK -> `role(id)`, ON DELETE RESTRICT | a user cannot exist without a role |
|
| `role_id` | INT UNSIGNED | NO | — | FK -> `role(id)`, ON DELETE RESTRICT | a user cannot exist without a role |
|
||||||
| `is_active` | TINYINT(1) | NO | 1 | — | deactivation without deletion |
|
| `is_active` | TINYINT(1) | NO | 1 | — | deactivation without deletion |
|
||||||
| `last_login_at` | DATETIME | YES | NULL | — | useful for audit and dormant account detection |
|
| `last_login_at` | DATETIME | YES | NULL | — | useful for audit and dormant account detection |
|
||||||
| `pin_hash` | VARCHAR(255) | YES | NULL | — | argon2id hash of the per-staff PIN that authorises sensitive actions (price/RBAC/user/cancel/inventory). NULL = no PIN set. Security-by-design, see note 13 |
|
|
||||||
| `failed_login_attempts` | SMALLINT UNSIGNED | NO | 0 | — | consecutive failed logins; drives degressive throttling (note 13) |
|
|
||||||
| `last_failed_login_at` | DATETIME | YES | NULL | — | timestamp of the last failed login |
|
|
||||||
| `lockout_until` | DATETIME | YES | NULL | — | end of the current throttling window (degressive backoff, not a hard indefinite lock) |
|
|
||||||
| `password_reset_token_hash` | VARCHAR(255) | YES | NULL | — | hash of the reset token (not the raw token); NULL when no reset pending |
|
|
||||||
| `password_reset_expires_at` | DATETIME | YES | NULL | — | expiry of the reset token |
|
|
||||||
| `anonymized_at` | DATETIME | YES | NULL | — | RGPD tombstone marker: when set, PII columns are nulled/replaced (note 13). The row is kept for referential integrity |
|
|
||||||
| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit |
|
| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | audit |
|
||||||
| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit |
|
| `updated_at` | DATETIME | NO | CURRENT_TIMESTAMP ON UPDATE | — | audit |
|
||||||
|
|
||||||
|
|
@ -412,10 +392,6 @@ are not authenticated and have no row here.
|
||||||
RFC 5321 email length: local-part <= 64, domain <= 255, total <= 254 (including `@`).
|
RFC 5321 email length: local-part <= 64, domain <= 255, total <= 254 (including `@`).
|
||||||
VARCHAR(254) is the spec-compliant value.
|
VARCHAR(254) is the spec-compliant value.
|
||||||
|
|
||||||
**PII columns**: `email`, `first_name`, `last_name`. Subject to RGPD anonymisation
|
|
||||||
(see note 13). `password_hash` and `pin_hash` are credentials, kept out of logs and
|
|
||||||
API responses.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.15 `role`
|
### 3.15 `role`
|
||||||
|
|
@ -563,59 +539,6 @@ Append-only audit log of all stock changes per ingredient.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.20 `audit_log`
|
|
||||||
|
|
||||||
Append-only log of **sensitive back-office actions**, for accountability where it matters
|
|
||||||
(insider threat, money handling, RBAC changes). Complements `stock_movement` (which is
|
|
||||||
stock-specific); covers catalogue/price, user, role/permission, and order cancellation events.
|
|
||||||
Security-by-design addition (see note 13).
|
|
||||||
|
|
||||||
| Attribute | Type | NULL | Default | Constraint | Notes |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | |
|
|
||||||
| `actor_user_id` | INT UNSIGNED | YES | NULL | FK -> `user(id)`, ON DELETE SET NULL | staff who performed the action, captured via PIN for sensitive operations. NULL if not attributable to an individual |
|
|
||||||
| `actor_role_id` | INT UNSIGNED | YES | NULL | FK -> `role(id)`, ON DELETE SET NULL | role context at action time (denormalised so the trail survives user anonymisation) |
|
|
||||||
| `action_code` | VARCHAR(60) | NO | — | INDEX | MCT operation / permission code, e.g. `product.update`, `order.cancel`, `role.manage`, `user.deactivate` |
|
|
||||||
| `entity_type` | VARCHAR(40) | YES | NULL | — | affected table name, e.g. `product`, `customer_order`, `role`, `user` |
|
|
||||||
| `entity_id` | INT UNSIGNED | YES | NULL | — | PK of the affected row |
|
|
||||||
| `summary` | VARCHAR(255) | YES | NULL | — | short non-personal description, e.g. "price_cents 880 -> 920", "added permission stock.manage" |
|
|
||||||
| `details` | JSON | YES | NULL | — | optional before/after diff. For user-targeted actions, stores changed **field names**, not PII values |
|
|
||||||
| `created_at` | DATETIME | NO | CURRENT_TIMESTAMP | INDEX | immutable timestamp |
|
|
||||||
|
|
||||||
**Immutability**: no UPDATE or DELETE at application layer (same discipline as `stock_movement`).
|
|
||||||
**Indexes**: `(actor_user_id, created_at)`, `(entity_type, entity_id)`, `(action_code, created_at)`.
|
|
||||||
**Retention**: own window (~12 months, legitimate-interest / fiscal traceability), decoupled
|
|
||||||
from user PII lifecycle (note 13). A scheduled purge (cron) removes rows past the window.
|
|
||||||
|
|
||||||
**Logged operations** (sensitive set): `UPDATE_PRODUCT` (8.2, incl. price), `DELETE_PRODUCT`
|
|
||||||
(8.3), `DELETE_MENU` (8.6), `CANCEL_ORDER` (7.1), `RESTOCK` (9.1), `INVENTORY_COUNT` (9.2),
|
|
||||||
`CREATE_USER` / `UPDATE_USER` / `DEACTIVATE_USER` (10.1-10.3), `MANAGE_RBAC` (10.4).
|
|
||||||
|
|
||||||
**Volume**: low (~10-50 sensitive actions/day) — orders of magnitude below `stock_movement`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.21 `login_throttle`
|
|
||||||
|
|
||||||
Per-source-IP brute-force throttle. Complements the per-account counter already on `user`
|
|
||||||
(`failed_login_attempts` / `lockout_until`), one row per source IP. Security-by-design addition
|
|
||||||
(see note 13).
|
|
||||||
|
|
||||||
| Attribute | Type | NULL | Default | Constraint | Notes |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| `id` | INT UNSIGNED | NO | AUTO_INCREMENT | PK | |
|
|
||||||
| `ip_address` | VARCHAR(45) | NO | — | UNIQUE | source IP, one row per IP, upserted; 45 chars holds a full IPv6 literal |
|
|
||||||
| `failed_attempts` | SMALLINT UNSIGNED | NO | 0 | — | consecutive failed logins from this IP in the current window |
|
|
||||||
| `window_started_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | start of the current counting window |
|
|
||||||
| `lockout_until` | DATETIME | YES | NULL | — | end of the degressive backoff window; NULL = not throttled |
|
|
||||||
| `last_attempt_at` | DATETIME | NO | CURRENT_TIMESTAMP | — | timestamp of the last failed attempt |
|
|
||||||
|
|
||||||
**No FK**: an IP is not a modelled entity. Rows are appended/upserted by IP; the window resets
|
|
||||||
when expired. A daily cron purges rows with no active lockout whose `last_attempt_at` is older
|
|
||||||
than 24h.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Modeling notes
|
## 4. Modeling notes
|
||||||
|
|
||||||
### Note 1 — Why `INT UNSIGNED` in cents for prices
|
### Note 1 — Why `INT UNSIGNED` in cents for prices
|
||||||
|
|
@ -825,62 +748,6 @@ The 4-state machine combined with 3 phase timestamps provides all KPI data neede
|
||||||
For stock audit, `stock_movement` (entity 3.19) provides the append-only audit trail
|
For stock audit, `stock_movement` (entity 3.19) provides the append-only audit trail
|
||||||
where it is genuinely needed (inventory reconciliation).
|
where it is genuinely needed (inventory reconciliation).
|
||||||
|
|
||||||
### Note 13 — Security-by-design data additions (2026-06-11)
|
|
||||||
|
|
||||||
These additions extend the prod-like model with a security-by-design layer. They do not
|
|
||||||
replace any v0.2 decision; they add accountability, auth lifecycle, and abuse resistance.
|
|
||||||
|
|
||||||
**Accountability — hybrid shared-account + PIN.** Back-office sessions stay shared per
|
|
||||||
workstation for the routine flow (a fast-food terminal is shared, `equipiers` rotate). A
|
|
||||||
per-staff PIN (`user.pin_hash`, argon2id) authorises a defined set of **sensitive actions**
|
|
||||||
(price/menu edits 8.2/8.3/8.6, order cancellation 7.1, inventory correction 9.2, user
|
|
||||||
management 10.1-10.3, RBAC 10.4). Those actions write the acting `user_id` into `audit_log`
|
|
||||||
(3.20). This resolves the circular justification that dropped `commande_event` in v0.1
|
|
||||||
(events were considered useless because accounts were shared): accountability is recorded
|
|
||||||
where it matters, at near-zero friction for the routine 95%. `customer_order.acting_user_id`
|
|
||||||
captures the staff for counter/drive orders taken under PIN; kiosk orders stay anonymous.
|
|
||||||
|
|
||||||
**Auth lifecycle.** `password_reset_token_hash` + `password_reset_expires_at` enable a reset
|
|
||||||
path (the token is stored hashed, the raw token is e-mailed once). Brute-force resistance uses
|
|
||||||
degressive throttling rather than a hard indefinite lock: `failed_login_attempts` +
|
|
||||||
`lockout_until` implement an exponential backoff per (account + source IP), so a fat-finger
|
|
||||||
streak does not lock out a whole kitchen mid-service (15 h continuous). Failed logins are
|
|
||||||
written to `audit_log`.
|
|
||||||
|
|
||||||
**RGPD anonymisation vs audit retention.** `user` PII (`email`, `first_name`, `last_name`)
|
|
||||||
is subject to the right to erasure (Cr 3.d). Erasure **anonymises** rather than hard-deletes:
|
|
||||||
the row is kept, `email` becomes a non-identifying unique placeholder (`anon-<id>@wakdo.invalid`,
|
|
||||||
RFC 2606 reserved domain), names are cleared, `password_hash`/`pin_hash` are invalidated, and
|
|
||||||
`anonymized_at` is set. The `audit_log` retains its own retention window (~12 months,
|
|
||||||
legitimate-interest / fiscal traceability) and keeps pointing at the anonymised principal, so
|
|
||||||
erasure and accountability coexist without breaking referential integrity.
|
|
||||||
|
|
||||||
**Abuse resistance on the anonymous kiosk.** `customer_order.idempotency_key` (client UUID,
|
|
||||||
UNIQUE) deduplicates a retried `POST /api/orders` so a network retry does not create a
|
|
||||||
duplicate paid order. Stock is decremented with a single atomic statement
|
|
||||||
(`UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id`): no operation
|
|
||||||
gates on a stock read, so the row self-locks for the duration of the write — no lost update and
|
|
||||||
no deadlock-ordering concern. This replaces the earlier pessimistic `SELECT ... FOR UPDATE`
|
|
||||||
approach (treatment-layer rule, see `mlt.md`); it adds no column here.
|
|
||||||
|
|
||||||
**Percentage stock model + computed availability.** `ingredient` carries `stock_capacity` (the
|
|
||||||
100% reference), `low_stock_pct` (warning band) and `critical_stock_pct` (auto-out-of-stock
|
|
||||||
floor) — see 3.6. `stock_quantity` is signed and may go negative (oversell magnitude surfaced to
|
|
||||||
managers); the system does not block an order on stock. Effective product orderability is
|
|
||||||
computed (rule RG-T21 in `mlt.md`): `product.is_available = 1` AND each non-removable
|
|
||||||
(`is_removable=0`) ingredient of its `product_ingredient` has
|
|
||||||
`stock_quantity > stock_capacity * critical_stock_pct/100`. At the critical band a product
|
|
||||||
auto-goes out-of-stock with no write and no cascade; a manual pull (`product.is_available=0`) is
|
|
||||||
a hard override; restock above the critical band makes the product orderable again on its own.
|
|
||||||
|
|
||||||
**Per-IP brute-force throttle.** `login_throttle` (3.21) tracks `failed_attempts` and
|
|
||||||
`lockout_until` per source IP (one upserted row per IP), complementing the per-account counter
|
|
||||||
on `user`. This adds a second throttling dimension so a single IP hammering many accounts is
|
|
||||||
slowed independently of any one account's counter. A daily cron purges idle, non-locked rows.
|
|
||||||
|
|
||||||
References: `docs/notes/revue-alignement-p1.md` §7 (D-decisions), security-by-design impact
|
|
||||||
map (2026-06-11). Threat model and data-classification matrix: `PROJECT_CONTEXT.md` §19 (to come).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Entity count summary
|
## 5. Entity count summary
|
||||||
|
|
@ -906,20 +773,11 @@ map (2026-06-11). Threat model and data-classification matrix: `PROJECT_CONTEXT.
|
||||||
| 17 | `permission` | reference | v0.1 `permission` (translated, catalogue frozen) |
|
| 17 | `permission` | reference | v0.1 `permission` (translated, catalogue frozen) |
|
||||||
| 18 | `role_permission` | join | v0.1 `role_permission` (unchanged) |
|
| 18 | `role_permission` | join | v0.1 `role_permission` (unchanged) |
|
||||||
| 19 | `stock_movement` | audit | new — append-only stock audit log |
|
| 19 | `stock_movement` | audit | new — append-only stock audit log |
|
||||||
| 20 | `audit_log` | audit | new (security-by-design) — append-only sensitive-action log |
|
|
||||||
| 21 | `login_throttle` | security | new (security-by-design) - per-IP brute-force throttle |
|
|
||||||
|
|
||||||
**Dropped from v0.1**: `commande_event` (replaced by phase timestamps on `customer_order`),
|
**Dropped from v0.1**: `commande_event` (replaced by phase timestamps on `customer_order`),
|
||||||
`menu_produit` (replaced by `menu_slot` + `menu_slot_option` model).
|
`menu_produit` (replaced by `menu_slot` + `menu_slot_option` model).
|
||||||
|
|
||||||
**Total: 21 entities** (19 prod-like v0.2 + `audit_log` and `login_throttle` from the
|
**Total: 19 entities.**
|
||||||
security-by-design layer).
|
|
||||||
|
|
||||||
Security-by-design also adds columns (beyond the two new entities): `user` auth-lifecycle +
|
|
||||||
`pin_hash` + `anonymized_at` (3.14), `customer_order.acting_user_id` + `idempotency_key` (3.10),
|
|
||||||
and the percentage stock model on `ingredient` (3.6) — `stock_capacity`, `critical_stock_pct`,
|
|
||||||
plus the rename of `low_stock_threshold` to `low_stock_pct`. `login_throttle` (3.21) is the 21st
|
|
||||||
entity. See note 13.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Conceptual Data Model (MCD) — Wakdo
|
# Conceptual Data Model (MCD) — Wakdo
|
||||||
|
|
||||||
**Merise phase** : P1 - Conception, step 2 (data dictionary first, mantra #33)
|
**Merise phase** : P1 - Conception, step 2 (data dictionary first, mantra #33)
|
||||||
**Version** : v0.2 — prod-like, 21 entities (19 prod-like + security-by-design layer)
|
**Version** : v0.2 — prod-like, 19 entities
|
||||||
**Date** : 2026-06-04 (security-by-design additions 2026-06-11)
|
**Date** : 2026-06-04
|
||||||
**Branch** : `feat/p1-conception`
|
**Branch** : `feat/p1-conception`
|
||||||
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7); security-by-design layer (audit_log + accountability/auth columns) in progress
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
**Author** : BYAN (methodology layer)
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -21,7 +21,7 @@ structure: how many X per Y, whether participation is mandatory, whether associa
|
||||||
their own attributes.
|
their own attributes.
|
||||||
|
|
||||||
**Sources**:
|
**Sources**:
|
||||||
- `docs/merise/dictionary.md` (v0.2 — 21 entities, source of truth for all names, types, ENUMs)
|
- `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/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/PROJECT_CONTEXT.md` (business rules: menu composition, order flow, RBAC, service modes)
|
||||||
- `docs/merise/_sources/` (school data: 9 categories, 53 products, 13 menus)
|
- `docs/merise/_sources/` (school data: 9 categories, 53 products, 13 menus)
|
||||||
|
|
@ -62,7 +62,7 @@ N-N associations that carry their own attributes become **associative entities**
|
||||||
|
|
||||||
## 3. Decomposition by sub-domain
|
## 3. Decomposition by sub-domain
|
||||||
|
|
||||||
The 21-entity model is split into 4 sub-domains for readability. Beyond approximately
|
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
|
5 entities, a single flat diagram becomes difficult to read; decomposition is the standard
|
||||||
Merise practice for models of this size.
|
Merise practice for models of this size.
|
||||||
|
|
||||||
|
|
@ -71,17 +71,9 @@ Merise practice for models of this size.
|
||||||
| Catalogue | category, product, menu, menu_slot, menu_slot_option | 5 |
|
| Catalogue | category, product, menu, menu_slot, menu_slot_option | 5 |
|
||||||
| Ingredients & Stock | ingredient, product_ingredient, allergen, ingredient_allergen, stock_movement | 5 |
|
| Ingredients & Stock | ingredient, product_ingredient, allergen, ingredient_allergen, stock_movement | 5 |
|
||||||
| Order | customer_order, order_item, order_item_selection, order_item_modifier | 4 |
|
| Order | customer_order, order_item, order_item_selection, order_item_modifier | 4 |
|
||||||
| RBAC & Audit | user, role, role_visible_source, permission, role_permission, audit_log, login_throttle | 7 |
|
| RBAC | user, role, role_visible_source, permission, role_permission | 5 |
|
||||||
|
|
||||||
> **Security-by-design layer (2026-06-11)**: `audit_log` (entity 20) is a cross-cutting,
|
**Note on the absence of a global diagram**: a single 19-entity ER diagram would be
|
||||||
> append-only log of sensitive actions; it is placed in the RBAC & Audit sub-domain because
|
|
||||||
> its references (`actor_user_id`, `actor_role_id`) are RBAC entities. `login_throttle`
|
|
||||||
> (entity 21) is a per-source-IP brute-force throttle, keyed by IP and carrying no FK; it sits
|
|
||||||
> in the same sub-domain because it guards the authentication path. New columns on existing
|
|
||||||
> entities: `user` auth-lifecycle + `pin_hash` + `anonymized_at`, `customer_order.acting_user_id`
|
|
||||||
> + `idempotency_key`. See dictionary note 13.
|
|
||||||
|
|
||||||
**Note on the absence of a global diagram**: a single 21-entity ER diagram would be
|
|
||||||
unreadable and unmaintainable. The sub-domain decomposition below is the intentional
|
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
|
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/`).
|
single reference once the MCD is stabilised (regeneration tracked in `docs/notes/`).
|
||||||
|
|
@ -182,18 +174,15 @@ erDiagram
|
||||||
varchar name
|
varchar name
|
||||||
varchar unit
|
varchar unit
|
||||||
int stock_quantity
|
int stock_quantity
|
||||||
int stock_capacity
|
|
||||||
smallint pack_size
|
smallint pack_size
|
||||||
varchar pack_label
|
varchar pack_label
|
||||||
smallint low_stock_pct
|
smallint low_stock_threshold
|
||||||
smallint critical_stock_pct
|
|
||||||
tinyint is_active
|
tinyint is_active
|
||||||
}
|
}
|
||||||
product_ingredient {
|
product_ingredient {
|
||||||
int product_id FK
|
int product_id FK
|
||||||
int ingredient_id FK
|
int ingredient_id FK
|
||||||
smallint quantity_normal
|
smallint quantity
|
||||||
smallint quantity_maxi
|
|
||||||
tinyint is_removable
|
tinyint is_removable
|
||||||
tinyint is_addable
|
tinyint is_addable
|
||||||
int extra_price_cents
|
int extra_price_cents
|
||||||
|
|
@ -249,15 +238,13 @@ erDiagram
|
||||||
|
|
||||||
### 5.3 Notes on the Ingredients & Stock sub-domain
|
### 5.3 Notes on the Ingredients & Stock sub-domain
|
||||||
|
|
||||||
**`product_ingredient` as an associative entity**: the N-N association between `product` and `ingredient` carries five attributes (`quantity_normal`, `quantity_maxi`, `is_removable`, `is_addable`, `extra_price_cents`). It becomes a join table in the MLD with composite PK `(product_id, ingredient_id)`.
|
**`product_ingredient` 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.
|
**`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`.
|
**`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`.
|
||||||
|
|
||||||
**Percentage-based stock model**: stock health is anchored on a per-ingredient `stock_capacity` (the 100% reference, `CHECK > 0`). `stock_quantity` is signed and may go negative when sales outrun counted stock; the system does not block an order on a low stock read. `stock_pct = ROUND(stock_quantity / stock_capacity * 100)` is computed, not stored. Two percentage thresholds drive a three-band behaviour: `low_stock_pct` (warning band, default 10%) and `critical_stock_pct` (auto-out-of-stock floor, default 5%), with the table-level invariant `critical_stock_pct < low_stock_pct`. Above the low band is normal; between critical and low the product stays orderable and a manager alert is raised (the manager either pulls the product via `product.is_available = 0` or restocks to clear the alert); at or below the critical band the product auto-goes out-of-stock (computed, see below).
|
**Low-stock alert**: computed at display time (`stock_quantity <= low_stock_threshold`); no additional stored column.
|
||||||
|
|
||||||
**Computed product availability (rule RG-T21, see `mlt.md`)**: effective orderability is derived, not stored. A product is orderable when `product.is_available = 1` AND each non-removable (`is_removable = 0`) ingredient in its `product_ingredient` has `stock_quantity > stock_capacity * critical_stock_pct / 100`. A required ingredient reaching the critical band makes the product auto-out-of-stock with no write and no cascade; a manual pull (`product.is_available = 0`) is a hard override; restock above the critical band makes the product orderable again on its own. A removable/optional ingredient at the critical band does not block the product (only its add-on becomes unavailable). The dashboard distinguishes a manual pull from a stock-driven OOS.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -270,9 +257,7 @@ erDiagram
|
||||||
customer_order {
|
customer_order {
|
||||||
int id PK
|
int id PK
|
||||||
varchar order_number
|
varchar order_number
|
||||||
varchar idempotency_key
|
|
||||||
enum source
|
enum source
|
||||||
int acting_user_id FK
|
|
||||||
enum service_mode
|
enum service_mode
|
||||||
enum status
|
enum status
|
||||||
int total_ht_cents
|
int total_ht_cents
|
||||||
|
|
@ -373,12 +358,6 @@ the MLD).
|
||||||
`preparing` and `ready` are dropped (decision D4, `revue-alignement-p1.md` §7). KPI timing is
|
`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`.
|
`delivered_at - paid_at`; KDS colour coding is computed from `NOW() - paid_at`.
|
||||||
|
|
||||||
**Security-by-design columns (2026-06-11)**: `idempotency_key` (client UUID, UNIQUE)
|
|
||||||
deduplicates a retried `POST /api/orders`. `acting_user_id` (FK -> `user`, ON DELETE SET NULL)
|
|
||||||
records the counter/drive staff who took the order under PIN; NULL for anonymous kiosk orders.
|
|
||||||
This adds a `customer_order |o--o| user : "taken_by"` association (cardinality: an order is
|
|
||||||
taken by (0,1) user; a user takes (0,N) orders). See dictionary note 13.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Sub-domain: RBAC
|
## 7. Sub-domain: RBAC
|
||||||
|
|
@ -391,15 +370,11 @@ erDiagram
|
||||||
int id PK
|
int id PK
|
||||||
varchar email
|
varchar email
|
||||||
varchar password_hash
|
varchar password_hash
|
||||||
varchar pin_hash
|
|
||||||
varchar first_name
|
varchar first_name
|
||||||
varchar last_name
|
varchar last_name
|
||||||
int role_id FK
|
int role_id FK
|
||||||
tinyint is_active
|
tinyint is_active
|
||||||
datetime last_login_at
|
datetime last_login_at
|
||||||
smallint failed_login_attempts
|
|
||||||
datetime lockout_until
|
|
||||||
datetime anonymized_at
|
|
||||||
}
|
}
|
||||||
role {
|
role {
|
||||||
int id PK
|
int id PK
|
||||||
|
|
@ -424,38 +399,13 @@ erDiagram
|
||||||
int role_id FK
|
int role_id FK
|
||||||
int permission_id FK
|
int permission_id FK
|
||||||
}
|
}
|
||||||
audit_log {
|
|
||||||
int id PK
|
|
||||||
int actor_user_id FK
|
|
||||||
int actor_role_id FK
|
|
||||||
varchar action_code
|
|
||||||
varchar entity_type
|
|
||||||
int entity_id
|
|
||||||
varchar summary
|
|
||||||
json details
|
|
||||||
datetime created_at
|
|
||||||
}
|
|
||||||
login_throttle {
|
|
||||||
int id PK
|
|
||||||
varchar ip_address UK
|
|
||||||
smallint failed_attempts
|
|
||||||
datetime window_started_at
|
|
||||||
datetime lockout_until
|
|
||||||
datetime last_attempt_at
|
|
||||||
}
|
|
||||||
|
|
||||||
user }o--|| role : "holds"
|
user }o--|| role : "holds"
|
||||||
role ||--o{ role_visible_source : "sees_source"
|
role ||--o{ role_visible_source : "sees_source"
|
||||||
role ||--o{ role_permission : "grants"
|
role ||--o{ role_permission : "grants"
|
||||||
permission ||--o{ role_permission : "granted_to"
|
permission ||--o{ role_permission : "granted_to"
|
||||||
user |o--o{ audit_log : "performs"
|
|
||||||
role |o--o{ audit_log : "context_of"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> `login_throttle` is a standalone entity with no association: it is keyed by source IP
|
|
||||||
> (`ip_address UNIQUE`), not by a modelled actor, so it carries no FK and connects to no
|
|
||||||
> other entity in the diagram.
|
|
||||||
|
|
||||||
### 7.2 Association cardinalities
|
### 7.2 Association cardinalities
|
||||||
|
|
||||||
| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification |
|
| # | Association | Side A | Cardinality A | Side B | Cardinality B | Justification |
|
||||||
|
|
@ -464,8 +414,6 @@ erDiagram
|
||||||
| 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. |
|
| 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. |
|
| 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. |
|
| R4 | granted_to | permission | (0,N) | role_permission | (1,1) | A permission may be granted to no roles yet (declared at seed, not yet distributed) or to several. Each mapping row references one permission. |
|
||||||
| R5 | performs | user | (0,1) | audit_log | (0,N) | A sensitive action captured under PIN records its acting user; automated/non-attributable entries carry NULL. A user may have logged any number of actions. ON DELETE SET NULL preserves the trail on user anonymisation/removal. |
|
|
||||||
| R6 | context_of | role | (0,1) | audit_log | (0,N) | Each audit row may denormalise the actor's role at action time (NULL allowed). A role may be the context of many audit rows. ON DELETE SET NULL preserves the trail. |
|
|
||||||
|
|
||||||
### 7.3 Notes on the RBAC sub-domain
|
### 7.3 Notes on the RBAC sub-domain
|
||||||
|
|
||||||
|
|
@ -482,28 +430,11 @@ erDiagram
|
||||||
**Seed roles** (5 roles, frozen at DDL; extendable without code change):
|
**Seed roles** (5 roles, frozen at DDL; extendable without code change):
|
||||||
`admin`, `manager`, `kitchen`, `counter`, `drive`.
|
`admin`, `manager`, `kitchen`, `counter`, `drive`.
|
||||||
|
|
||||||
**`audit_log` (security-by-design)**: append-only log of sensitive actions, immutable like
|
|
||||||
`stock_movement`. Both FKs (`actor_user_id`, `actor_role_id`) are nullable with ON DELETE
|
|
||||||
SET NULL, so the trail survives user anonymisation (RGPD) and role removal. The `actor_role_id`
|
|
||||||
is denormalised on purpose: even if the user is later anonymised, the role context of the
|
|
||||||
action is preserved. It carries no PII (the `details` JSON stores changed field names, not
|
|
||||||
values for user-targeted actions). See dictionary 3.20 and note 13.
|
|
||||||
|
|
||||||
**`login_throttle` (security-by-design)**: per-source-IP brute-force throttle, complementing
|
|
||||||
the per-account counter already on `user` (`failed_login_attempts` / `lockout_until`). One row
|
|
||||||
per IP (`ip_address VARCHAR(45) UNIQUE`, 45 chars to hold a full IPv6 literal), upserted on each
|
|
||||||
failed login: `failed_attempts` counts consecutive failures from this IP in the current window,
|
|
||||||
`window_started_at` marks the start of that window (which resets when expired), `lockout_until`
|
|
||||||
holds the end of the degressive backoff (NULL = not throttled), `last_attempt_at` the timestamp
|
|
||||||
of the last failed attempt. It has no FK (an IP is not a modelled entity) and no association. A
|
|
||||||
daily cron purges rows with no active lockout whose `last_attempt_at` is older than 24h. See
|
|
||||||
dictionary 3.21 and note 13.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Cross-validation MCD <-> dictionary
|
## 8. Cross-validation MCD <-> dictionary
|
||||||
|
|
||||||
Verification that all 21 dictionary entities appear in the MCD and vice versa.
|
Verification that all 19 dictionary entities appear in the MCD and vice versa.
|
||||||
|
|
||||||
| # | Dictionary entity (section 3) | Sub-domain in MCD | Present |
|
| # | Dictionary entity (section 3) | Sub-domain in MCD | Present |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|
@ -526,21 +457,17 @@ Verification that all 21 dictionary entities appear in the MCD and vice versa.
|
||||||
| 17 | `permission` (3.17) | RBAC | Yes |
|
| 17 | `permission` (3.17) | RBAC | Yes |
|
||||||
| 18 | `role_permission` (3.18) | RBAC | Yes |
|
| 18 | `role_permission` (3.18) | RBAC | Yes |
|
||||||
| 19 | `stock_movement` (3.19) | Ingredients & Stock | Yes |
|
| 19 | `stock_movement` (3.19) | Ingredients & Stock | Yes |
|
||||||
| 20 | `audit_log` (3.20) | RBAC & Audit | Yes |
|
|
||||||
| 21 | `login_throttle` (3.21) | RBAC & Audit | Yes |
|
|
||||||
|
|
||||||
**Result**: 21/21 entities traced (19 prod-like + `audit_log` and `login_throttle`
|
**Result**: 19/19 entities traced. No entity from the dictionary is absent from the MCD.
|
||||||
security-by-design). No entity from the dictionary is absent from the MCD. No entity in the MCD
|
No entity in the MCD falls outside the dictionary.
|
||||||
falls outside the dictionary.
|
|
||||||
|
|
||||||
**Entities appearing in multiple sub-domains** (cross-domain shared entities):
|
**Entities appearing in multiple sub-domains** (cross-domain shared entities):
|
||||||
- `product`: Catalogue (sold item, slot eligibility) + Ingredients (recipe) + Order (line reference, slot choice)
|
- `product`: Catalogue (sold item, slot eligibility) + Ingredients (recipe) + Order (line reference, slot choice)
|
||||||
- `menu`: Catalogue (definition, slots) + Order (line reference)
|
- `menu`: Catalogue (definition, slots) + Order (line reference)
|
||||||
- `menu_slot`: Catalogue (slot definition) + Order (slot choices via `order_item_selection`)
|
- `menu_slot`: Catalogue (slot definition) + Order (slot choices via `order_item_selection`)
|
||||||
- `ingredient`: Ingredients (recipe, stock) + Order (modifiers)
|
- `ingredient`: Ingredients (recipe, stock) + Order (modifiers)
|
||||||
- `customer_order`: Order (order lifecycle) + Ingredients (stock movement trigger) + RBAC & Audit (taken_by staff via `acting_user_id`)
|
- `customer_order`: Order (order lifecycle) + Ingredients (stock movement trigger)
|
||||||
- `user`: RBAC (authentication) + Ingredients (stock movement author) + Order (`acting_user_id` on counter/drive orders) + Audit (actor of `audit_log`)
|
- `user`: RBAC (authentication) + Ingredients (stock movement author)
|
||||||
- `role`: RBAC (permissions, visible sources) + Audit (denormalised `actor_role_id` context on `audit_log`)
|
|
||||||
|
|
||||||
This is expected in a normalised model. The sub-domain split is for readability; the actual
|
This is expected in a normalised model. The sub-domain split is for readability; the actual
|
||||||
relational schema is a unified graph.
|
relational schema is a unified graph.
|
||||||
|
|
@ -591,12 +518,9 @@ Pre-validation: each entity participates in at least one treatment.
|
||||||
| `permission` | Admin permission matrix management |
|
| `permission` | Admin permission matrix management |
|
||||||
| `role_permission` | Admin permission matrix management |
|
| `role_permission` | Admin permission matrix management |
|
||||||
| `stock_movement` | Automatic at `paid` transition; manual restock and inventory correction |
|
| `stock_movement` | Automatic at `paid` transition; manual restock and inventory correction |
|
||||||
| `audit_log` | Written by sensitive operations: UPDATE/DELETE product/menu (8.2/8.3/8.6), CANCEL_ORDER (7.1), RESTOCK/INVENTORY_COUNT (9.1/9.2), user ops (10.1-10.3), MANAGE_RBAC (10.4), and failed/successful logins (12.1) |
|
|
||||||
| `login_throttle` | Read and written by AUTHENTICATE_USER (12.1): per-source-IP throttle upserted on each failed login, read to enforce the backoff window, purged by a daily cron |
|
|
||||||
|
|
||||||
Cross-validation MCD <-> MCT (mantra #34) to be completed exhaustively in `mct.md`
|
Cross-validation MCD <-> MCT (mantra #34) to be completed exhaustively in `mct.md`
|
||||||
once the MCT incorporates the security-by-design operations (PIN-gated sensitive actions,
|
once the MCT is updated to the 4-state machine and 19-entity model.
|
||||||
audit writes, reset/lockout, anonymisation). The treatment-layer additions are tracked there.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Model of Conceptual Treatments (MCT) — Wakdo
|
# Model of Conceptual Treatments (MCT) — Wakdo
|
||||||
|
|
||||||
**Merise phase** : P1 - Conception, step 3 (after MCD)
|
**Merise phase** : P1 - Conception, step 3 (after MCD)
|
||||||
**Version** : v0.2 — prod-like, 4-state machine (+ security-by-design layer 2026-06-11)
|
**Version** : v0.2 — prod-like, 4-state machine
|
||||||
**Date** : 2026-06-04 (security-by-design additions 2026-06-11)
|
**Date** : 2026-06-04
|
||||||
**Branch** : `feat/p1-conception`
|
**Branch** : `feat/p1-conception`
|
||||||
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7); security-by-design ops added (ERASE_USER_PII, RESET_PASSWORD, PIN-gated sensitive set, audit_log writes, auth throttling) — 28 operations
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
**Author** : BYAN (methodology layer)
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -56,17 +56,6 @@ the ticket and act. The single staff gesture is "deliver". KPI is total time
|
||||||
and `MARK_READY` (`MARQUER_PRETE`) are removed because their intermediate states no longer
|
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.
|
exist. `DELIVER_ORDER` becomes the sole status-advancing action for counter/drive staff.
|
||||||
|
|
||||||
**Security-by-design layer (2026-06-11)**: two operations are added — `RESET_PASSWORD` (12.3)
|
|
||||||
and `ERASE_USER_PII` (10.5, RGPD anonymisation). A subset of operations is **PIN-gated**:
|
|
||||||
back-office sessions stay shared per workstation, but a per-staff PIN re-authorises the
|
|
||||||
sensitive set — `CANCEL_ORDER` (7.1), `UPDATE_PRODUCT`/`DELETE_PRODUCT` (8.2/8.3),
|
|
||||||
`DELETE_MENU` (8.6), `INVENTORY_COUNT` (9.2), user management (10.1-10.3), `MANAGE_RBAC`
|
|
||||||
(10.4), `ERASE_USER_PII` (10.5). These non-stock actions append an immutable `audit_log` row
|
|
||||||
(actor, action, target); stock actions record attribution in `stock_movement`. The treatment
|
|
||||||
logic (PIN, audit, throttling, idempotency, atomic stock decrement, computed product
|
|
||||||
availability) is specified in `mlt.md` (rules RG-T13-T21). This adds entities 20 `audit_log`
|
|
||||||
and 21 `login_throttle` to the model.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Representation conventions
|
## 2. Representation conventions
|
||||||
|
|
@ -113,7 +102,7 @@ For each operation the document provides:
|
||||||
| **Synchronisation** | None (single event) |
|
| **Synchronisation** | None (single event) |
|
||||||
| **Condition** | The kiosk is in service (within business hours 10:00-01:00) |
|
| **Condition** | The kiosk is in service (within business hours 10:00-01:00) |
|
||||||
| **Operation** | LOAD_CATALOGUE |
|
| **Operation** | LOAD_CATALOGUE |
|
||||||
| **Description** | Retrieval of active categories, available products, and available menus (with their slots and eligible options) for display on the kiosk screen. Product availability is COMPUTED: a product is orderable only if its `is_available` flag is set AND each non-removable (`is_removable=0`) ingredient in its `product_ingredient` is above the critical band (`stock_quantity > stock_capacity * critical_stock_pct/100`). See rule RG-T21 in `mlt.md`. |
|
| **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` |
|
| **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 |
|
| **Result** | Catalogue loaded; kiosk displays the home screen |
|
||||||
|
|
||||||
|
|
@ -348,7 +337,7 @@ For each operation the document provides:
|
||||||
| **Synchronisation** | OR (create ingredient, update ingredient, update composition, update allergen mapping) |
|
| **Synchronisation** | OR (create ingredient, update ingredient, update composition, update allergen mapping) |
|
||||||
| **Condition** | Actor holds permission `ingredient.manage`. |
|
| **Condition** | Actor holds permission `ingredient.manage`. |
|
||||||
| **Operation** | MANAGE_INGREDIENT |
|
| **Operation** | MANAGE_INGREDIENT |
|
||||||
| **Description** | CRUD on `ingredient` (name, unit, pack_size, pack_label, stock_capacity, low_stock_pct, critical_stock_pct, is_active). Manage `product_ingredient` composition (quantity_normal, quantity_maxi, is_removable, is_addable, extra_price_cents) for any product. Manage `ingredient_allergen` mapping (14 EU regulated allergens). Deactivating an ingredient (`is_active=0`) hides it from the configurator without deletion. Physical deletion of `ingredient` is blocked if referenced in `product_ingredient` (FK `ON DELETE RESTRICT`) or `stock_movement` (FK `ON DELETE RESTRICT`). |
|
| **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) |
|
| **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 |
|
| **Result** | Ingredient / composition / allergen mapping updated |
|
||||||
|
|
||||||
|
|
@ -395,7 +384,7 @@ For each operation the document provides:
|
||||||
| **Synchronisation** | None |
|
| **Synchronisation** | None |
|
||||||
| **Condition** | Actor holds permission `stock.read`. |
|
| **Condition** | Actor holds permission `stock.read`. |
|
||||||
| **Operation** | READ_STOCK |
|
| **Operation** | READ_STOCK |
|
||||||
| **Description** | Read `ingredient` list with current `stock_quantity`, `stock_capacity`, computed `stock_pct`, `low_stock_pct`, `critical_stock_pct`, `pack_size`, `pack_label`. Stock bands computed at display time: `low_stock` when `stock_quantity <= stock_capacity * low_stock_pct/100`, `critical_stock` when `stock_quantity <= stock_capacity * critical_stock_pct/100`. Optional: read `stock_movement` history for a given ingredient, filtered by date range. |
|
| **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) |
|
| **MCD entities** | R: `ingredient`, `stock_movement` (optional history) |
|
||||||
| **Result** | Stock list displayed with low-stock indicators |
|
| **Result** | Stock list displayed with low-stock indicators |
|
||||||
|
|
||||||
|
|
@ -463,21 +452,6 @@ For each operation the document provides:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10.5 ERASE_USER_PII (security-by-design)
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Triggering event** | A RGPD erasure request is processed for a back-office user |
|
|
||||||
| **Actor** | ADMIN (PIN-gated) |
|
|
||||||
| **Synchronisation** | None |
|
|
||||||
| **Condition** | Actor holds permission `user.update` and has re-authorised via PIN. Target user exists and is not already anonymised. |
|
|
||||||
| **Operation** | ERASE_USER_PII |
|
|
||||||
| **Description** | RGPD right-to-erasure honoured by **anonymisation**, not physical deletion: PII (`email`, `first_name`, `last_name`) is cleared/replaced by a non-identifying placeholder, credentials invalidated, `anonymized_at` set. The row persists so referential links (`stock_movement`, `customer_order`, `audit_log`) stay valid and resolve to an anonymised principal. See `mlt.md` 10.5 and dictionary note 13. |
|
|
||||||
| **MCD entities** | W: `user` (UPDATE — PII cleared, `anonymized_at` set), `audit_log` (INSERT) |
|
|
||||||
| **Result** | User anonymised; PII removed; accountability links preserved; one `audit_log` row recorded |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Domain 9 — Stats and KPI
|
## 11. Domain 9 — Stats and KPI
|
||||||
|
|
||||||
### 11.1 READ_STATS
|
### 11.1 READ_STATS
|
||||||
|
|
@ -504,11 +478,11 @@ For each operation the document provides:
|
||||||
| **Triggering event** | An actor submits the login form |
|
| **Triggering event** | An actor submits the login form |
|
||||||
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
|
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
|
||||||
| **Synchronisation** | None |
|
| **Synchronisation** | None |
|
||||||
| **Condition** | Account not in a throttling window (`lockout_until`). Email exists in database. Password matches argon2id hash. User `is_active=1`. |
|
| **Condition** | Email exists in database. Password matches argon2id hash. User `is_active=1`. |
|
||||||
| **Operation** | AUTHENTICATE_USER |
|
| **Operation** | AUTHENTICATE_USER |
|
||||||
| **Description** | Credential verification. If valid: session ID regeneration (protection against session fixation), storage of `user_id` and `role_id` in session, UPDATE `last_login_at`, reset of the login failure counter. On failure: increment `failed_login_attempts` and apply a degressive backoff (`lockout_until`), enumeration-safe generic error. Idle timeout: 4h. Absolute timeout: 10h. Redirect to `role.default_route`. See `mlt.md` 12.1. |
|
| **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`, `login_throttle` (the per-IP throttle gate) — W: `user` (UPDATE last_login_at, `failed_login_attempts`, `lockout_until`), `login_throttle` (upsert `failed_attempts`/`lockout_until` on failure, clear on success), `audit_log` (INSERT login success/failure) |
|
| **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; or throttled failure logged |
|
| **Result** | Session opened, redirect to role-specific default view |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -527,21 +501,6 @@ For each operation the document provides:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 12.3 RESET_PASSWORD (security-by-design)
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| **Triggering event** | A user requests a password reset, then confirms it via the emailed link |
|
|
||||||
| **Actor** | COUNTER / DRIVE / KITCHEN / MANAGER / ADMIN |
|
|
||||||
| **Synchronisation** | Sequential two-phase: request, then confirm |
|
|
||||||
| **Condition** | Request: the submitted email is processed enumeration-safely (same neutral response whether or not it exists). Confirm: a valid, non-expired token is presented. |
|
|
||||||
| **Operation** | RESET_PASSWORD |
|
|
||||||
| **Description** | Request phase generates a random token, stores its hash + expiry, and e-mails the raw token once. Confirm phase validates the token hash + expiry, replaces `password_hash` (argon2id), clears the token, and resets the login failure counter. See `mlt.md` 12.3. |
|
|
||||||
| **MCD entities** | W: `user` (UPDATE `password_reset_token_hash` + `password_reset_expires_at` on request; UPDATE `password_hash`, clear token, reset `failed_login_attempts`/`lockout_until` on confirm), `audit_log` (INSERT) |
|
|
||||||
| **Result** | Password reset via a one-time, time-bound token; one `audit_log` row recorded |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. State machine — customer_order.status
|
## 13. State machine — customer_order.status
|
||||||
|
|
||||||
Summary of transitions covered by MCT operations.
|
Summary of transitions covered by MCT operations.
|
||||||
|
|
@ -614,17 +573,8 @@ single delivery action (DELIVER_ORDER) collapses the v0.1 three-step sequence in
|
||||||
| 24 | READ_STATS | Stats | MANAGER/ADMIN | — | customer_order, order_item |
|
| 24 | READ_STATS | Stats | MANAGER/ADMIN | — | customer_order, order_item |
|
||||||
| 25 | AUTHENTICATE_USER | Auth | ALL BACK | user | user, role, role_permission |
|
| 25 | AUTHENTICATE_USER | Auth | ALL BACK | user | user, role, role_permission |
|
||||||
| 26 | LOGOUT_USER | Auth | ALL BACK | — | — |
|
| 26 | LOGOUT_USER | Auth | ALL BACK | — | — |
|
||||||
| 27 | ERASE_USER_PII | RBAC | ADMIN | user, audit_log | user |
|
|
||||||
| 28 | RESET_PASSWORD | Auth | ALL BACK | user, audit_log | user |
|
|
||||||
|
|
||||||
**Total: 28 operations** (26 prod-like + `ERASE_USER_PII` and `RESET_PASSWORD` from the
|
**Total: 26 operations** covering the complete Wakdo business lifecycle.
|
||||||
security-by-design layer).
|
|
||||||
|
|
||||||
**Audit log writes (security-by-design)**: the sensitive operations 7.1 (cancel), 8.2/8.3
|
|
||||||
(product update/delete), 8.6 (menu delete), 10.1-10.5 (user/RBAC/erasure) and 12.1 (login)
|
|
||||||
also write an `audit_log` row (W entity not repeated per row above to keep the table legible).
|
|
||||||
Stock operations 9.1/9.2 record their attribution via `stock_movement.user_id`. PIN-gated set
|
|
||||||
per `mlt.md` RG-T13.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -653,20 +603,9 @@ Verification that each MCD entity participates in at least one MCT operation.
|
||||||
| `permission` | 23 | — (static seed) | OK (*) |
|
| `permission` | 23 | — (static seed) | OK (*) |
|
||||||
| `role_permission` | 25 | 23 | OK |
|
| `role_permission` | 25 | 23 | OK |
|
||||||
| `stock_movement` | 19 | 3, 5, 8, 17, 18 | OK |
|
| `stock_movement` | 19 | 3, 5, 8, 17, 18 | OK |
|
||||||
| `audit_log` | (admin audit view) | 8, 10, 11, 14, 20, 21, 22, 23, 25, 27, 28 | OK |
|
|
||||||
| `login_throttle` | 25 | 25 | OK |
|
|
||||||
|
|
||||||
(*) `allergen` and `permission` are read-only at the MCT level: their values are declared
|
(*) `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
|
in seed migrations and are not modifiable via the UI. `allergen` is managed indirectly
|
||||||
via `ingredient_allergen` in MANAGE_INGREDIENT.
|
via `ingredient_allergen` in MANAGE_INGREDIENT.
|
||||||
|
|
||||||
(**) `audit_log` (entity 20, security-by-design) is write-mostly: it is appended by the
|
**Conclusion**: 19/19 entities covered. MCT <-> MCD consistency validated.
|
||||||
sensitive operations above and read through an admin audit view (a dedicated read operation
|
|
||||||
can be formalised when the audit UI is specified at P3).
|
|
||||||
|
|
||||||
(***) `login_throttle` (entity 21, security-by-design) is the per-source-IP brute-force
|
|
||||||
throttle gate: it is read AND written (upserted) by `AUTHENTICATE_USER` (25). Its daily purge
|
|
||||||
of stale rows is a cron, documented in `mlt.md`, outside MCT operation scope.
|
|
||||||
|
|
||||||
**Conclusion**: 21/21 entities covered (19 prod-like + `audit_log` + `login_throttle`). MCT <-> MCD consistency
|
|
||||||
validated.
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Logical Data Model (MLD) — Wakdo
|
# Logical Data Model (MLD) — Wakdo
|
||||||
|
|
||||||
**Merise phase** : P1 - Conception, step 5 (after MCD, MCT, MLT)
|
**Merise phase** : P1 - Conception, step 5 (after MCD, MCT, MLT)
|
||||||
**Version** : v0.2 — prod-like, 21 tables (19 prod-like + security-by-design layer)
|
**Version** : v0.2 — prod-like, 19 tables
|
||||||
**Date** : 2026-06-04 (security-by-design additions 2026-06-11)
|
**Date** : 2026-06-04
|
||||||
**Branch** : `feat/p1-conception`
|
**Branch** : `feat/p1-conception`
|
||||||
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7); security-by-design layer (audit_log + accountability/auth columns) in progress
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
**Author** : BYAN (methodology layer)
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -93,7 +93,7 @@ in addition to the composite FK PK. Applied to `product_ingredient`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Relational schema (21 tables)
|
## 4. Relational schema (19 tables)
|
||||||
|
|
||||||
Tables are ordered by dependency (no-FK tables first, then tables that depend on them).
|
Tables are ordered by dependency (no-FK tables first, then tables that depend on them).
|
||||||
|
|
||||||
|
|
@ -245,16 +245,14 @@ No timestamps. Pure join table.
|
||||||
### 4.6 `ingredient`
|
### 4.6 `ingredient`
|
||||||
|
|
||||||
```
|
```
|
||||||
ingredient (id, name, unit, stock_quantity, stock_capacity, pack_size, [pack_label],
|
ingredient (id, name, unit, stock_quantity, pack_size, [pack_label],
|
||||||
low_stock_pct, critical_stock_pct, is_active, created_at, updated_at)
|
low_stock_threshold, is_active, created_at, updated_at)
|
||||||
|
|
||||||
PK : id
|
PK : id
|
||||||
UK : name
|
UK : name
|
||||||
CHK : stock_capacity > 0
|
CHK : stock_quantity >= 0
|
||||||
CHK : pack_size > 0
|
CHK : pack_size > 0
|
||||||
CHK : low_stock_pct BETWEEN 0 AND 100
|
CHK : low_stock_threshold >= 0
|
||||||
CHK : critical_stock_pct BETWEEN 0 AND 100
|
|
||||||
CHK : critical_stock_pct < low_stock_pct
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| Column | Type | NULL | Notes |
|
| Column | Type | NULL | Notes |
|
||||||
|
|
@ -262,38 +260,16 @@ ingredient (id, name, unit, stock_quantity, stock_capacity, pack_size, [pack_lab
|
||||||
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
| `name` | VARCHAR(120) | NO | Unique name, e.g. "Sesame Bun" |
|
| `name` | VARCHAR(120) | NO | Unique name, e.g. "Sesame Bun" |
|
||||||
| `unit` | VARCHAR(40) | NO | Packaging unit label (free-form, not ENUM) |
|
| `unit` | VARCHAR(40) | NO | Packaging unit label (free-form, not ENUM) |
|
||||||
| `stock_quantity` | INT NOT NULL DEFAULT 0 | NO | Current stock. Signed INT that may go negative when sales outrun counted stock (oversell magnitude, surfaced to managers); the system does not block an order on stock |
|
| `stock_quantity` | INT NOT NULL DEFAULT 0 | NO | Current stock. Signed INT to detect negative (alert) |
|
||||||
| `stock_capacity` | INT NOT NULL | NO | Reference "full" level in units = the 100% used to compute the stock percentage; CHECK > 0 also guards the percentage division against divide-by-zero |
|
|
||||||
| `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units per restocking pack |
|
| `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Units per restocking pack |
|
||||||
| `pack_label` | VARCHAR(80) | YES | Human label of the pack |
|
| `pack_label` | VARCHAR(80) | YES | Human label of the pack |
|
||||||
| `low_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 10 | NO | Warning band, percent of capacity (CHECK BETWEEN 0 AND 100) |
|
| `low_stock_threshold` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Alert threshold |
|
||||||
| `critical_stock_pct` | SMALLINT UNSIGNED NOT NULL DEFAULT 5 | NO | Auto-out-of-stock floor, percent of capacity (CHECK BETWEEN 0 AND 100; table CHECK `critical_stock_pct < low_stock_pct`) |
|
|
||||||
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivate obsolete ingredients |
|
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivate obsolete ingredients |
|
||||||
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
No FK. Root table for the Ingredients & Stock sub-domain.
|
No FK. Root table for the Ingredients & Stock sub-domain.
|
||||||
|
|
||||||
**Percentage-based stock model**: the stock state is computed (NOT stored) as
|
|
||||||
`stock_pct = ROUND(stock_quantity / stock_capacity * 100)`. Two bands derive from it:
|
|
||||||
`LOW` when `stock_quantity <= stock_capacity * low_stock_pct/100`, and
|
|
||||||
`CRITICAL` when `stock_quantity <= stock_capacity * critical_stock_pct/100`.
|
|
||||||
Three-band behaviour: above `low` = normal; between `critical` and `low` = orderable
|
|
||||||
plus manager alert (the manager either pulls the product via `product.is_available=0`, or
|
|
||||||
restocks to clear the alert); at or below `critical` = auto out-of-stock (computed, rule
|
|
||||||
RG-T21). `stock_quantity` is signed and may go negative; the system does not block an order
|
|
||||||
on stock, so a negative value records the oversell magnitude for managers.
|
|
||||||
|
|
||||||
**Computed availability (rule RG-T21)**: a product is effectively orderable when
|
|
||||||
`product.is_available = 1` AND each non-removable (`is_removable=0`) ingredient in its
|
|
||||||
`product_ingredient` has `stock_quantity > stock_capacity * critical_stock_pct/100`. At the
|
|
||||||
critical band a product auto-goes out-of-stock with no write and no cascade; a manual pull
|
|
||||||
(`product.is_available=0`) is a hard override; a restock above the critical band makes the
|
|
||||||
product orderable again on its own; a removable/optional ingredient at the critical band does
|
|
||||||
not block the product (only its add-on becomes unavailable). The dashboard distinguishes a
|
|
||||||
manual pull (`is_available=0`) from a stock-driven OOS (`is_available=1` but a required
|
|
||||||
ingredient is critical).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.7 `product_ingredient`
|
### 4.7 `product_ingredient`
|
||||||
|
|
@ -406,10 +382,8 @@ No FK. Root table for RBAC.
|
||||||
### 4.11 `user`
|
### 4.11 `user`
|
||||||
|
|
||||||
```
|
```
|
||||||
user (id, email, password_hash, [pin_hash], first_name, last_name, #role_id,
|
user (id, email, password_hash, first_name, last_name, #role_id,
|
||||||
is_active, [last_login_at], failed_login_attempts, [last_failed_login_at],
|
is_active, [last_login_at], created_at, updated_at)
|
||||||
[lockout_until], [password_reset_token_hash], [password_reset_expires_at],
|
|
||||||
[anonymized_at], created_at, updated_at)
|
|
||||||
|
|
||||||
PK : id
|
PK : id
|
||||||
UK : email
|
UK : email
|
||||||
|
|
@ -420,32 +394,19 @@ user (id, email, password_hash, [pin_hash], first_name, last_name, #role_id,
|
||||||
| Column | Type | NULL | Notes |
|
| Column | Type | NULL | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
| `email` | VARCHAR(254) | NO | RFC 5321 max length. PII (RGPD anonymisation, see below) |
|
| `email` | VARCHAR(254) | NO | RFC 5321 max length |
|
||||||
| `password_hash` | VARCHAR(255) | NO | argon2id hash |
|
| `password_hash` | VARCHAR(255) | NO | argon2id hash |
|
||||||
| `pin_hash` | VARCHAR(255) | YES | argon2id hash of the per-staff PIN (sensitive-action authorisation). Security-by-design |
|
| `first_name` | VARCHAR(60) | NO | |
|
||||||
| `first_name` | VARCHAR(60) | NO | PII |
|
| `last_name` | VARCHAR(60) | NO | |
|
||||||
| `last_name` | VARCHAR(60) | NO | PII |
|
|
||||||
| `role_id` | INT UNSIGNED | NO | FK -> role |
|
| `role_id` | INT UNSIGNED | NO | FK -> role |
|
||||||
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivation without deletion |
|
| `is_active` | TINYINT(1) NOT NULL DEFAULT 1 | NO | Deactivation without deletion |
|
||||||
| `last_login_at` | DATETIME | YES | Audit, dormant account detection |
|
| `last_login_at` | DATETIME | YES | Audit, dormant account detection |
|
||||||
| `failed_login_attempts` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Brute-force counter (degressive throttling) |
|
|
||||||
| `last_failed_login_at` | DATETIME | YES | Timestamp of last failed login |
|
|
||||||
| `lockout_until` | DATETIME | YES | End of current throttling window (backoff, not indefinite lock) |
|
|
||||||
| `password_reset_token_hash` | VARCHAR(255) | YES | Hash of the reset token (not the raw token) |
|
|
||||||
| `password_reset_expires_at` | DATETIME | YES | Reset token expiry |
|
|
||||||
| `anonymized_at` | DATETIME | YES | RGPD tombstone marker; PII nulled/replaced when set |
|
|
||||||
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Audit |
|
||||||
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
**ON DELETE RESTRICT** on `role_id`: a role cannot be deleted while users hold it.
|
**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.
|
Deactivate the role first (`is_active = 0`), then reassign users before deleting.
|
||||||
|
|
||||||
**RGPD anonymisation** (security-by-design, dict. note 13): the right to erasure is honoured by
|
|
||||||
anonymising, not hard-deleting. `email` becomes a unique non-identifying placeholder
|
|
||||||
(`anon-<id>@wakdo.invalid`, RFC 2606 reserved domain — preserves the UNIQUE constraint),
|
|
||||||
`first_name`/`last_name` are cleared, `password_hash`/`pin_hash` are invalidated, `is_active=0`,
|
|
||||||
`anonymized_at = NOW()`. The row persists so `audit_log` and `stock_movement` FKs stay valid.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.12 `role_visible_source`
|
### 4.12 `role_visible_source`
|
||||||
|
|
@ -527,16 +488,13 @@ No timestamps. Pure join table.
|
||||||
### 4.15 `customer_order`
|
### 4.15 `customer_order`
|
||||||
|
|
||||||
```
|
```
|
||||||
customer_order (id, order_number, [idempotency_key], source, [#acting_user_id],
|
customer_order (id, order_number, source, service_mode, status,
|
||||||
service_mode, status,
|
|
||||||
total_ht_cents, total_vat_cents, total_ttc_cents,
|
total_ht_cents, total_vat_cents, total_ttc_cents,
|
||||||
[paid_at], [delivered_at], [cancelled_at],
|
[paid_at], [delivered_at], [cancelled_at],
|
||||||
created_at, updated_at)
|
created_at, updated_at)
|
||||||
|
|
||||||
PK : id
|
PK : id
|
||||||
UK : order_number
|
UK : order_number
|
||||||
UK : idempotency_key
|
|
||||||
FK : acting_user_id -> user(id) ON DELETE SET NULL
|
|
||||||
IDX : (status, created_at)
|
IDX : (status, created_at)
|
||||||
IDX : (source, created_at)
|
IDX : (source, created_at)
|
||||||
IDX : created_at
|
IDX : created_at
|
||||||
|
|
@ -551,9 +509,7 @@ customer_order (id, order_number, [idempotency_key], source, [#acting_user_id],
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
||||||
| `order_number` | VARCHAR(20) | NO | Format `K/C/D-YYYY-MM-DD-NNN` by channel |
|
| `order_number` | VARCHAR(20) | NO | Format `K/C/D-YYYY-MM-DD-NNN` by channel |
|
||||||
| `idempotency_key` | VARCHAR(36) | YES | Client UUID, UNIQUE; deduplicates retried POST (security-by-design) |
|
|
||||||
| `source` | ENUM('kiosk','counter','drive') | NO | Input channel |
|
| `source` | ENUM('kiosk','counter','drive') | NO | Input channel |
|
||||||
| `acting_user_id` | INT UNSIGNED | YES | FK -> user; counter/drive staff under PIN; NULL for kiosk |
|
|
||||||
| `service_mode` | ENUM('dine_in','takeaway','drive') | NO | Consumption mode (stats only, no fiscal role) |
|
| `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 |
|
| `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_ht_cents` | INT UNSIGNED | NO | Ex-VAT total snapshot |
|
||||||
|
|
@ -565,11 +521,8 @@ customer_order (id, order_number, [idempotency_key], source, [#acting_user_id],
|
||||||
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Used as `service_day` base |
|
| `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 |
|
| `updated_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | NO | Audit |
|
||||||
|
|
||||||
**Staff attribution (security-by-design)**: `acting_user_id` (FK -> `user`, ON DELETE SET NULL)
|
No FK toward `user`: staff attribution is not stored on the order. Operational accountability
|
||||||
records the counter/drive staff who took the order under PIN; NULL for anonymous kiosk orders.
|
is covered by `stock_movement.user_id` for stock actions.
|
||||||
Kiosk orders stay anonymous by design. `stock_movement.user_id` covers attribution of stock
|
|
||||||
actions. `idempotency_key` (UNIQUE, nullable) deduplicates a retried `POST /api/orders`
|
|
||||||
(multiple NULLs allowed by the UNIQUE index, so non-idempotent legacy paths are tolerated).
|
|
||||||
|
|
||||||
**4-state machine**: `pending_payment -> paid -> delivered` (+ `cancelled`). States `preparing`
|
**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).
|
and `ready` are dropped (decision D4). KPI: `delivered_at - paid_at` (target SLA ~10 min).
|
||||||
|
|
@ -740,76 +693,6 @@ No `updated_at`. Immutable append-only table.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4.20 `audit_log`
|
|
||||||
|
|
||||||
Append-only log of sensitive back-office actions (security-by-design, dict. 3.20).
|
|
||||||
|
|
||||||
```
|
|
||||||
audit_log (id, [#actor_user_id], [#actor_role_id], action_code,
|
|
||||||
[entity_type], [entity_id], [summary], [details], created_at)
|
|
||||||
|
|
||||||
PK : id
|
|
||||||
FK : actor_user_id -> user(id) ON DELETE SET NULL
|
|
||||||
FK : actor_role_id -> role(id) ON DELETE SET NULL
|
|
||||||
IDX : (actor_user_id, created_at)
|
|
||||||
IDX : (entity_type, entity_id)
|
|
||||||
IDX : (action_code, created_at)
|
|
||||||
```
|
|
||||||
|
|
||||||
| Column | Type | NULL | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
|
||||||
| `actor_user_id` | INT UNSIGNED | YES | FK -> user; acting staff (PIN-captured) or NULL if not attributable |
|
|
||||||
| `actor_role_id` | INT UNSIGNED | YES | FK -> role; denormalised role context (survives user anonymisation) |
|
|
||||||
| `action_code` | VARCHAR(60) | NO | MCT operation / permission code, e.g. `product.update`, `order.cancel` |
|
|
||||||
| `entity_type` | VARCHAR(40) | YES | Affected table name |
|
|
||||||
| `entity_id` | INT UNSIGNED | YES | PK of the affected row |
|
|
||||||
| `summary` | VARCHAR(255) | YES | Short non-personal change description |
|
|
||||||
| `details` | JSON | YES | Optional before/after diff (field names for user-targeted actions, not PII values) |
|
|
||||||
| `created_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Immutable timestamp |
|
|
||||||
|
|
||||||
**ON DELETE SET NULL** on both FKs: the trail is preserved when a user is anonymised/removed
|
|
||||||
or a role deleted; only the link is severed (the denormalised `actor_role_id` keeps the role
|
|
||||||
context even after user anonymisation).
|
|
||||||
|
|
||||||
**Immutability rule**: no UPDATE or DELETE at application layer. **Retention**: a scheduled
|
|
||||||
cron purge removes rows older than the retention window (~12 months, legitimate-interest /
|
|
||||||
fiscal traceability), decoupled from the user PII lifecycle (dict. note 13).
|
|
||||||
|
|
||||||
No `updated_at`. Immutable append-only table.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.21 `login_throttle`
|
|
||||||
|
|
||||||
Per-source-IP brute-force throttle (security-by-design). Complements the per-account counter
|
|
||||||
already on `user` (`failed_login_attempts` / `lockout_until`).
|
|
||||||
|
|
||||||
```
|
|
||||||
login_throttle (id, ip_address, failed_attempts, window_started_at,
|
|
||||||
[lockout_until], last_attempt_at)
|
|
||||||
|
|
||||||
PK : id
|
|
||||||
UK : ip_address
|
|
||||||
IDX : lockout_until
|
|
||||||
```
|
|
||||||
|
|
||||||
| Column | Type | NULL | Notes |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `id` | INT UNSIGNED AUTO_INCREMENT | NO | PK |
|
|
||||||
| `ip_address` | VARCHAR(45) | NO | Source IP, one row per IP, upserted; 45 chars holds a full IPv6 literal. UNIQUE |
|
|
||||||
| `failed_attempts` | SMALLINT UNSIGNED NOT NULL DEFAULT 0 | NO | Consecutive failed logins from this IP in the current window |
|
|
||||||
| `window_started_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Start of the current counting window |
|
|
||||||
| `lockout_until` | DATETIME | YES | End of the degressive backoff window; NULL = not throttled |
|
|
||||||
| `last_attempt_at` | DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | NO | Timestamp of the last failed attempt |
|
|
||||||
|
|
||||||
No FK: an IP is not a modelled entity. Append/upsert by IP; the window resets when expired. A
|
|
||||||
daily cron purges rows with no active lockout whose `last_attempt_at` is older than 24h.
|
|
||||||
|
|
||||||
No `updated_at`: rows are upserted by IP, not edited through a UI.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Referential integrity summary
|
## 5. Referential integrity summary
|
||||||
|
|
||||||
| FK column | References | ON DELETE | Rationale |
|
| FK column | References | ON DELETE | Rationale |
|
||||||
|
|
@ -839,9 +722,6 @@ No `updated_at`: rows are upserted by IP, not edited through a UI.
|
||||||
| `stock_movement.ingredient_id` | `ingredient(id)` | RESTRICT | Ingredient with history cannot be deleted |
|
| `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.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 |
|
| `stock_movement.user_id` | `user(id)` | SET NULL | Audit preserved, user attribution lost |
|
||||||
| `customer_order.acting_user_id` | `user(id)` | SET NULL | Staff attribution preserved as anonymised principal; order kept |
|
|
||||||
| `audit_log.actor_user_id` | `user(id)` | SET NULL | Audit trail preserved on user anonymisation; only the link is severed |
|
|
||||||
| `audit_log.actor_role_id` | `role(id)` | SET NULL | Role context kept until role deletion; denormalised so it survives user anonymisation |
|
|
||||||
|
|
||||||
**Key used**: CASCADE = child has no meaning without parent; RESTRICT = parent deletion
|
**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.
|
blocked while children exist; SET NULL = child is preserved, only the link is severed.
|
||||||
|
|
@ -856,11 +736,9 @@ blocked while children exist; SET NULL = child is preserved, only the link is se
|
||||||
| `product` | `vat_rate IN (55, 100)` | Only two legal VAT rates for this model |
|
| `product` | `vat_rate IN (55, 100)` | Only two legal VAT rates for this model |
|
||||||
| `menu` | `price_normal_cents > 0` | Same as product |
|
| `menu` | `price_normal_cents > 0` | Same as product |
|
||||||
| `menu` | `price_maxi_cents > 0` | Same |
|
| `menu` | `price_maxi_cents > 0` | Same |
|
||||||
| `ingredient` | `stock_capacity > 0` | The 100% reference must be positive; also guards the percentage division against divide-by-zero |
|
| `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` | `pack_size > 0` | Pack size of zero makes restock logic incoherent |
|
||||||
| `ingredient` | `low_stock_pct BETWEEN 0 AND 100` | Warning band is a percent of capacity |
|
| `ingredient` | `low_stock_threshold >= 0` | Threshold cannot be negative |
|
||||||
| `ingredient` | `critical_stock_pct BETWEEN 0 AND 100` | Auto-out-of-stock floor is a percent of capacity |
|
|
||||||
| `ingredient` | `critical_stock_pct < low_stock_pct` | Critical floor sits below the warning band |
|
|
||||||
| `product_ingredient` | `quantity_normal > 0` | Recipe quantity of zero is meaningless |
|
| `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` | `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 |
|
| `product_ingredient` | `extra_price_cents >= 0` | No negative surcharge |
|
||||||
|
|
@ -898,10 +776,6 @@ MCT / MLT.
|
||||||
| `stock_movement` | `(movement_type, created_at)` | Stats: cancellations per week, restocks per month |
|
| `stock_movement` | `(movement_type, created_at)` | Stats: cancellations per week, restocks per month |
|
||||||
| `role_permission` | `permission_id` | Reverse query: "which roles have this permission?" |
|
| `role_permission` | `permission_id` | Reverse query: "which roles have this permission?" |
|
||||||
| `user` | `(is_active, role_id)` | Login check + permission resolution |
|
| `user` | `(is_active, role_id)` | Login check + permission resolution |
|
||||||
| `audit_log` | `(actor_user_id, created_at)` | Per-actor audit history |
|
|
||||||
| `audit_log` | `(entity_type, entity_id)` | "what happened to this product/order/user?" |
|
|
||||||
| `audit_log` | `(action_code, created_at)` | Audit by action type over a time range |
|
|
||||||
| `login_throttle` | `lockout_until` | Daily cron purge of rows with no active lockout |
|
|
||||||
|
|
||||||
**Indexes not added** (intentional):
|
**Indexes not added** (intentional):
|
||||||
- `customer_order.order_number`: UK index is sufficient; no range query expected on this column.
|
- `customer_order.order_number`: UK index is sufficient; no range query expected on this column.
|
||||||
|
|
@ -913,8 +787,7 @@ MCT / MLT.
|
||||||
|
|
||||||
## 8. Cross-validation MLD <-> MCD
|
## 8. Cross-validation MLD <-> MCD
|
||||||
|
|
||||||
Verification that all 21 MCD entities (19 prod-like + 2 security-by-design) map to a table,
|
Verification that all 19 MCD entities map to a table, and that all tables trace to the MCD.
|
||||||
and that all tables trace to the MCD.
|
|
||||||
|
|
||||||
| MCD entity | MLD table | Mapping type | Notes |
|
| MCD entity | MLD table | Mapping type | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|
@ -937,14 +810,8 @@ and that all tables trace to the MCD.
|
||||||
| `order_item_selection` (C17) | `order_item_selection` (4.17) | 1:1 entity | New entity (v0.2) |
|
| `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) |
|
| `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) |
|
| `stock_movement` (C19) | `stock_movement` (4.19) | 1:1 entity | New entity (v0.2) |
|
||||||
| `audit_log` (R5/R6) | `audit_log` (4.20) | 1:1 entity | New entity (security-by-design) |
|
|
||||||
| `login_throttle` (R7) | `login_throttle` (4.21) | 1:1 entity | New entity (security-by-design) |
|
|
||||||
|
|
||||||
**Result**: 21/21 entities mapped (19 prod-like + `audit_log` + `login_throttle`). No entity
|
**Result**: 19/19 entities mapped. No entity without a table; no table outside the MCD.
|
||||||
without a table; no table outside the MCD. New columns on existing tables: `user`
|
|
||||||
(auth-lifecycle + `pin_hash` + `anonymized_at`), `customer_order` (`idempotency_key`,
|
|
||||||
`acting_user_id`), `ingredient` (`stock_capacity`, `low_stock_pct`, `critical_stock_pct`;
|
|
||||||
`low_stock_threshold` repurposed).
|
|
||||||
|
|
||||||
**Dropped from v0.1**: `commande_event` (replaced by `paid_at`, `delivered_at`, `cancelled_at`
|
**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
|
phase timestamps on `customer_order` — decision 2.A); `menu_produit` fixed-composition model
|
||||||
|
|
@ -975,11 +842,8 @@ phase timestamps on `customer_order` — decision 2.A); `menu_produit` fixed-com
|
||||||
| `order_item_selection` | ~300k | 150 bytes | ~45 MB |
|
| `order_item_selection` | ~300k | 150 bytes | ~45 MB |
|
||||||
| `order_item_modifier` | ~150k | 80 bytes | ~12 MB |
|
| `order_item_modifier` | ~150k | 80 bytes | ~12 MB |
|
||||||
| `stock_movement` | ~500k | 180 bytes | ~90 MB |
|
| `stock_movement` | ~500k | 180 bytes | ~90 MB |
|
||||||
| `audit_log` | ~5k-10k | 200 bytes | ~2 MB |
|
|
||||||
| `login_throttle` | ~100-1k | 80 bytes | < 1 MB |
|
|
||||||
|
|
||||||
**Estimated total**: ~190 MB data + ~60-80 MB for indexes = ~250-270 MB over 6 months
|
**Estimated total**: ~190 MB data + ~60-80 MB for indexes = ~250-270 MB over 6 months.
|
||||||
(`audit_log` is negligible: sensitive actions are orders of magnitude rarer than orders).
|
|
||||||
Manageable on the MariaDB container (`wakdo_db_data` named volume in `docker-compose.yml`).
|
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).
|
`stock_movement` is the highest-volume table (~5-15 rows per order across all ingredients).
|
||||||
|
|
@ -1025,11 +889,6 @@ history; it will carry meaningful write amplification at scale.
|
||||||
- `order_item_selection` (depends on `order_item`, `menu_slot`, `product`)
|
- `order_item_selection` (depends on `order_item`, `menu_slot`, `product`)
|
||||||
- `order_item_modifier` (depends on `order_item`, `ingredient`)
|
- `order_item_modifier` (depends on `order_item`, `ingredient`)
|
||||||
- `stock_movement` (depends on `ingredient`, `customer_order`, `user`)
|
- `stock_movement` (depends on `ingredient`, `customer_order`, `user`)
|
||||||
- `audit_log` (depends on `user`, `role`)
|
|
||||||
- `login_throttle` (no FK, can be created at any point)
|
|
||||||
|
|
||||||
Note: `customer_order` now carries `acting_user_id -> user`, so `user` must be created
|
|
||||||
before `customer_order` (already the case: the RBAC block precedes `customer_order`).
|
|
||||||
|
|
||||||
2. **Seed** (`db/seeds/0001_demo_data.sql`):
|
2. **Seed** (`db/seeds/0001_demo_data.sql`):
|
||||||
- 9 categories + 53 products + 13 menus from JSON sources (`docs/merise/_sources/`)
|
- 9 categories + 53 products + 13 menus from JSON sources (`docs/merise/_sources/`)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Model of Logical Treatments (MLT) — Wakdo
|
# Model of Logical Treatments (MLT) — Wakdo
|
||||||
|
|
||||||
**Merise phase** : P1 - Conception, step 4 (derived from MCT)
|
**Merise phase** : P1 - Conception, step 4 (derived from MCT)
|
||||||
**Version** : v0.2 — prod-like, 4-state machine (+ security-by-design layer 2026-06-11)
|
**Version** : v0.2 — prod-like, 4-state machine
|
||||||
**Date** : 2026-06-04 (security-by-design additions 2026-06-11)
|
**Date** : 2026-06-04
|
||||||
**Branch** : `feat/p1-conception`
|
**Branch** : `feat/p1-conception`
|
||||||
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7); security-by-design rules added (RG-T13-T21: PIN, audit, escaping, allowlists, idempotency, atomic decrement, computed product availability (RG-T21); ops RESET_PASSWORD, ERASE_USER_PII, auth throttling; per-IP throttle table `login_throttle`)
|
**Status** : prod-like — all D1-D8 + stock decisions applied (see `docs/notes/revue-alignement-p1.md` §7)
|
||||||
**Author** : BYAN (methodology layer)
|
**Author** : BYAN (methodology layer)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -49,15 +49,6 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **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-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-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 |
|
| **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 |
|
||||||
| **RG-T13** | **Sensitive-action PIN** (security-by-design): the set of sensitive operations requires a per-staff PIN re-authorisation before execution: verify the submitted PIN against `user.pin_hash` (`password_verify`, argon2id). On success the acting `user_id` is captured for the audit log; on failure the operation is rejected. Sensitive set: 7.1 (cancel), 8.2/8.3 (product update/delete), 8.6 (menu delete), 9.2 (inventory correction), 10.1/10.2/10.3 (user mgmt), 10.4 (RBAC), 10.5 (PII erasure). Sessions stay shared per workstation for the routine 95%. | 7.1, 8.2, 8.3, 8.6, 9.2, 10.1-10.5 |
|
|
||||||
| **RG-T14** | **Audit log write**: non-stock sensitive operations append one immutable `audit_log` row in the same transaction as their effect: `actor_user_id` (from RG-T13 PIN), `actor_role_id`, `action_code` (permission/operation code), `entity_type` + `entity_id` of the affected row, `summary` (non-personal change description), `details` JSON (changed field **names** for user-targeted actions, not PII values). No UPDATE/DELETE on `audit_log`. Stock actions (9.1 restock, 9.2 inventory) record their attribution via `stock_movement.user_id` (PIN-captured), which already provides the append-only stock trail — they are not double-logged. | 7.1, 8.2, 8.3, 8.6, 10.1-10.5, 12.1 |
|
|
||||||
| **RG-T15** | **Output escaping** (anti-XSS): free-text fields (`product.name`/`description`, `ingredient.name`, `user.first_name`/`last_name`, notes) are context-escaped at render. Server-rendered admin views use `htmlspecialchars($v, ENT_QUOTES, 'UTF-8')`; the vanilla-JS kiosk injects text via `textContent` (or an explicit escaper), not `innerHTML`. | All views rendering stored text |
|
|
||||||
| **RG-T16** | **Mass-assignment allowlist**: INSERT/UPDATE statements bind only an explicit per-operation column allowlist from the request; extra/unknown fields are dropped. Prevents tampering with `price_cents`, `vat_rate`, `role_id`, `is_active`, `status` via injected form fields. | 8.1, 8.2, 8.4, 8.5, 10.1, 10.2 |
|
|
||||||
| **RG-T17** | **Dynamic identifier allowlist**: column/direction tokens used in dynamic `ORDER BY` / `GROUP BY` are resolved against a fixed allowlist of column names before query build (RG-T06 covers values via bind parameters; SQL identifiers cannot be bound, so they are allowlisted). | 5.1, 9.3, 11.1 |
|
|
||||||
| **RG-T18** | **Server-side validation and length bounds**: every input is re-validated server-side regardless of client checks — type, range, max length (matching the dictionary VARCHAR sizes), enum membership, FK existence. Client-side validation is a UX aid, not a trust boundary. | All write operations |
|
|
||||||
| **RG-T19** | **Idempotency**: `POST /api/orders` carries a client-generated `idempotency_key` (UUID). Before creating, look it up on `customer_order.idempotency_key` (UNIQUE); if a row exists, return that order instead of creating a duplicate (replayed network retry). | 3.3, 4.1 |
|
|
||||||
| **RG-T20** | **Atomic stock decrement**: during the `paid` transition, each affected `ingredient` is decremented with a single self-locking statement `UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id` — no preceding read-gate, no `SELECT ... FOR UPDATE`. Concurrent orders on the same ingredient apply their deltas without a lost update and without a deadlock-ordering concern. `stock_quantity` is signed and may go negative when sales outrun counted stock (oversell magnitude surfaced to managers); the decrement does not block on a floor. | 3.3, 4.1 |
|
|
||||||
| **RG-T21** | **Computed product availability**: a product's effective orderability is computed, not stored. It is orderable when `product.is_available = 1` AND each non-removable (`is_removable = 0`) ingredient in its `product_ingredient` has `stock_quantity > stock_capacity * critical_stock_pct / 100`. At the critical band a required ingredient takes the product out-of-stock with no write and no cascade; restock above the critical band makes it orderable again on its own; a manual pull (`product.is_available = 0`) is a hard override; a removable/optional ingredient at the critical band does not block the product (only its add-on becomes unavailable). | 3.1, 3.3, 4.1, 5.1 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -116,13 +107,10 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[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-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-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-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); apply the atomic decrement `UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id` (single self-locking statement, no preceding read-gate, RG-T20); `stock_quantity` is signed and may go negative (oversell magnitude, surfaced to managers) — the decrement does not gate on a floor; 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-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-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). |
|
| **[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). |
|
||||||
| **[RG-8 — idempotency]** | The body carries a client `idempotency_key` (UUID). Before any write, `SELECT id, order_number, status FROM customer_order WHERE idempotency_key = :key`. If found, skip creation and return that order (deduplicates a replayed retry — RG-T19). The key is stored on the new `customer_order` row. |
|
| **[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. |
|
||||||
| **[RG-9 — server-side modifier re-validation]** | The ingredient modifiers in the body are re-validated server-side against `product_ingredient`: an `action='remove'` requires `is_removable=1`; an `action='add'` requires `is_addable=1` and snapshots the current `extra_price_cents`. Client-side checks (3.2 RG-4) are not trusted; a crafted POST adding a non-addable ingredient is rejected (HTTP 422). |
|
|
||||||
| **[RG-10 — atomic stock decrement]** | No operation gates on a stock read, so the decrement is a single atomic statement `UPDATE ingredient SET stock_quantity = stock_quantity - :units WHERE id = :id` (RG-T20). The row self-locks for the duration of the update, so concurrent kiosk orders on the same ingredient apply their deltas without a lost update and without a deadlock-ordering concern; `stock_quantity` is signed and may go negative (oversell magnitude surfaced to managers). |
|
|
||||||
| **[POST-1]** | One `customer_order` row exists with `status = 'paid'`, `source = 'kiosk'`, all totals computed, `paid_at` set, `idempotency_key` stored. 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-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-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. |
|
| **[POST-4]** | `ingredient.stock_quantity` decremented for each consumed ingredient unit; one `stock_movement` row of type `sale` per affected ingredient. |
|
||||||
|
|
@ -164,8 +152,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[RG-2 — cross-constraint]** | If `source = 'drive'` then `service_mode` must be `'drive'` (RG-T09); verified before INSERT. HTTP 422 if violated. |
|
| **[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-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. |
|
| **[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. |
|
||||||
| **[RG-5 — staff attribution + decrement]** | `customer_order.acting_user_id` is set to the authenticated staff member's id (targeted accountability on counter/drive orders; kiosk orders stay NULL). Server-side modifier re-validation (3.3 RG-9), idempotency (RG-T19) and the atomic stock decrement (RG-T20) apply identically. No PIN is required to create an order (the `order.create` permission suffices); order creation is not in the sensitive-action set. |
|
| **[POST-1]** | One `customer_order` row with `status = 'paid'`, `source = 'counter'` or `'drive'`, `paid_at` set. |
|
||||||
| **[POST-1]** | One `customer_order` row with `status = 'paid'`, `source = 'counter'` or `'drive'`, `paid_at` set, `acting_user_id` set. |
|
|
||||||
| **[POST-2]** | N `order_item` rows with snapshots. Slot selections and modifiers written identically to kiosk flow. |
|
| **[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`. |
|
| **[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. |
|
| **[OUT-1]** | HTTP 201: `{data: {id: int, order_number: string, status: 'paid'}}`. Order number communicated to customer. |
|
||||||
|
|
@ -231,8 +218,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[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-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-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. |
|
| **[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. |
|
||||||
| **[RG-6 — PIN + audit]** | Cancellation is a sensitive money-handling action: it requires the per-staff PIN (RG-T13) and writes one `audit_log` row in the same transaction (RG-T14): `action_code='order.cancel'`, `entity_type='customer_order'`, `entity_id=:id`, `summary` with prior status and re-credited amount. |
|
| **[POST-1]** | `customer_order.status = 'cancelled'`, `cancelled_at` set, terminal state. |
|
||||||
| **[POST-1]** | `customer_order.status = 'cancelled'`, `cancelled_at` set, terminal state. One `audit_log` row recorded with the acting staff. |
|
|
||||||
| **[POST-2]** | If prior status was `paid`: `ingredient.stock_quantity` re-credited; one `stock_movement` row of type `cancellation` per affected ingredient. |
|
| **[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 |
|
| **[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-1]** | Attempt to cancel a delivered or already cancelled order: HTTP 422, `{error: {code: "CANNOT_CANCEL_IN_STATE", current_status: "..."}}` |
|
||||||
|
|
@ -272,8 +258,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[RG-1]** | Same validations as CREATE_PRODUCT on modified fields |
|
| **[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-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) |
|
| **[RG-3]** | `label_snapshot`, `unit_price_cents_snapshot`, `vat_rate_snapshot` in historical `order_item` rows are not modified (see RG-T05) |
|
||||||
| **[RG-4 — PIN + audit + allowlist]** | A price/VAT change is a sensitive action: it requires the per-staff PIN (RG-T13) and writes one `audit_log` row (RG-T14) with `action_code='product.update'`, `entity_type='product'`, `entity_id=:id`, and a `summary` recording changed values (e.g. `price_cents 880 -> 920`). Only the allowlisted columns (`name`, `description`, `price_cents`, `vat_rate`, `image_path`, `is_available`, `display_order`, `category_id`) are bound from the request (RG-T16). |
|
| **[POST-1]** | `product` updated, `updated_at` refreshed |
|
||||||
| **[POST-1]** | `product` updated, `updated_at` refreshed; one `audit_log` row recorded |
|
|
||||||
| **[OUT-1]** | Redirect to product list with success message |
|
| **[OUT-1]** | Redirect to product list with success message |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -290,8 +275,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[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-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-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. |
|
| **[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. |
|
||||||
| **[RG-5 — PIN + audit]** | Deletion is a sensitive action: it requires the per-staff PIN (RG-T13) and writes one `audit_log` row (RG-T14) with `action_code='product.delete'`, `entity_type='product'`, `entity_id=:id`, `summary` capturing the product name before deletion (recorded before the row is removed). |
|
| **[POST-1]** | Product deleted if no FK constraint was blocking |
|
||||||
| **[POST-1]** | Product deleted if no FK constraint was blocking; one `audit_log` row recorded |
|
|
||||||
| **[OUT-1]** | Redirect to product list with success message |
|
| **[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-1]** | Product in menu slot: HTTP 422 or inline message with blocking menu list |
|
||||||
| **[ERR-2]** | Product in historical orders: message proposing deactivation instead |
|
| **[ERR-2]** | Product in historical orders: message proposing deactivation instead |
|
||||||
|
|
@ -343,8 +327,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[PRE-2]** | Target `menu.id` exists |
|
| **[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-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`) |
|
| **[RG-2]** | If no historical reference: DELETE `menu` triggers CASCADE to `menu_slot` (which cascades to `menu_slot_option`) |
|
||||||
| **[RG-3 — PIN + audit]** | Deletion is a sensitive action: per-staff PIN (RG-T13) + one `audit_log` row (RG-T14), `action_code='menu.delete'`, `entity_type='menu'`, `entity_id=:id`, `summary` capturing the menu name before deletion. |
|
| **[POST-1]** | `menu`, its `menu_slot` rows, and its `menu_slot_option` rows deleted |
|
||||||
| **[POST-1]** | `menu`, its `menu_slot` rows, and its `menu_slot_option` rows deleted; one `audit_log` row recorded |
|
|
||||||
| **[OUT-1]** | Redirect with success message |
|
| **[OUT-1]** | Redirect with success message |
|
||||||
| **[ERR-1]** | Menu in historical orders: message proposing deactivation instead |
|
| **[ERR-1]** | Menu in historical orders: message proposing deactivation instead |
|
||||||
|
|
||||||
|
|
@ -374,8 +357,8 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| Tag | Content |
|
| Tag | Content |
|
||||||
|-----|---------|
|
|-----|---------|
|
||||||
| **[PRE-1]** | Actor authenticated, holds permission `ingredient.manage` |
|
| **[PRE-1]** | Actor authenticated, holds permission `ingredient.manage` |
|
||||||
| **[RG-CREATE-ING]** | `name` non-empty and UNIQUE; `unit` non-empty; `pack_size >= 1`; `stock_capacity >= 1` (the 100% reference); `low_stock_pct` and `critical_stock_pct` in 0-100 with `critical_stock_pct < low_stock_pct` (defaults 10 / 5); `stock_quantity` defaults to 0 at creation |
|
| **[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`, `stock_capacity`, `low_stock_pct`, `critical_stock_pct`, `is_active` |
|
| **[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-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-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). |
|
| **[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). |
|
||||||
|
|
@ -415,8 +398,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[RG-1]** | `delta = actual_quantity - ingredient.stock_quantity` (may be negative if actual < theoretical) |
|
| **[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-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 |
|
| **[RG-3]** | `delta = 0` is a valid correction (physical count matches theoretical); a movement row is still inserted for audit completeness |
|
||||||
| **[RG-4 — PIN attribution]** | An inventory correction can mask shrinkage, so it requires the per-staff PIN (RG-T13). The PIN-captured `user_id` is written to `stock_movement.user_id`, making the correction attributable to a person even on a shared workstation. No separate `audit_log` row (the `stock_movement` trail already records it). |
|
| **[POST-1]** | `ingredient.stock_quantity = actual_quantity`. One `stock_movement` row of type `inventory_correction` inserted. |
|
||||||
| **[POST-1]** | `ingredient.stock_quantity = actual_quantity`. One `stock_movement` row of type `inventory_correction` inserted with the acting `user_id`. |
|
|
||||||
| **[OUT-1]** | Confirmation with reconciled stock level and discrepancy displayed |
|
| **[OUT-1]** | Confirmation with reconciled stock level and discrepancy displayed |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -429,11 +411,10 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
|-----|---------|
|
|-----|---------|
|
||||||
| **[PRE-1]** | Actor authenticated, holds permission `stock.read` |
|
| **[PRE-1]** | Actor authenticated, holds permission `stock.read` |
|
||||||
| **[RG-1]** | `SELECT * FROM ingredient WHERE is_active = 1 ORDER BY name ASC` |
|
| **[RG-1]** | `SELECT * FROM ingredient WHERE is_active = 1 ORDER BY name ASC` |
|
||||||
| **[RG-2]** | Stock bands computed at render time from the percentage thresholds: `low_stock: true` when `stock_quantity <= stock_capacity * low_stock_pct / 100`, `critical_stock: true` when `stock_quantity <= stock_capacity * critical_stock_pct / 100`; `stock_pct = ROUND(stock_quantity / stock_capacity * 100)` is also returned. Not stored as columns. |
|
| **[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` |
|
| **[RG-3]** | Optional movement history for a given ingredient: `SELECT * FROM stock_movement WHERE ingredient_id = :id ORDER BY created_at DESC LIMIT :n` |
|
||||||
| **[RG-4 — attribution visibility]** | The `stock_movement.user_id` (who restocked / who corrected) is included for `manager`/`admin` only; line staff (`kitchen`/`counter`/`drive`) see the movement deltas without the actor identity. This limits intra-team exposure while preserving accountability for those who manage. The `details` allowlist is applied at the query/serialisation layer. |
|
|
||||||
| **[POST-1]** | No database write |
|
| **[POST-1]** | No database write |
|
||||||
| **[OUT-1]** | Ingredient list with `stock_quantity`, `stock_capacity`, computed `stock_pct`, `low_stock_pct`, `critical_stock_pct`, `pack_size`, `pack_label`, `low_stock` / `critical_stock` flags; movement history with actor visible to manager/admin only |
|
| **[OUT-1]** | Ingredient list with `stock_quantity`, `low_stock_threshold`, `pack_size`, `pack_label`, `low_stock` flag |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -451,8 +432,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[RG-1]** | Validation: `email` conforms to RFC 5321 (PHP `FILTER_VALIDATE_EMAIL`), `first_name` and `last_name` non-empty, `role_id` valid |
|
| **[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-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 |
|
| **[RG-3]** | `is_active = 1` by default; `last_login_at = NULL` at creation |
|
||||||
| **[RG-4 — PIN + audit + allowlist]** | Creating a back-office account is a sensitive action: per-staff PIN (RG-T13) + one `audit_log` row (RG-T14), `action_code='user.create'`, `entity_type='user'`, `entity_id=:new_id`, `details` recording the assigned `role_id` (field names/role, not the password). Only the allowlisted columns are bound (RG-T16): `email`, `first_name`, `last_name`, `role_id` (+ the hashed password); `is_active` and any other field are server-set, not request-bound. |
|
| **[POST-1]** | One `user` row with argon2id `password_hash`, valid `role_id` |
|
||||||
| **[POST-1]** | One `user` row with argon2id `password_hash`, valid `role_id`; one `audit_log` row recorded |
|
|
||||||
| **[OUT-1]** | Redirect to user list with success message |
|
| **[OUT-1]** | Redirect to user list with success message |
|
||||||
| **[ERR-1]** | Duplicate email: message "This email is already in use" |
|
| **[ERR-1]** | Duplicate email: message "This email is already in use" |
|
||||||
| **[ERR-2]** | Password too short: inline validation message |
|
| **[ERR-2]** | Password too short: inline validation message |
|
||||||
|
|
@ -470,8 +450,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[RG-1]** | If a new password is supplied (non-empty field): rehash via `PASSWORD_ARGON2ID` and replace existing hash |
|
| **[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-2]** | If password field is empty: existing hash is preserved unchanged |
|
||||||
| **[RG-3]** | Email update subject to UNIQUE constraint (pre-check before UPDATE) |
|
| **[RG-3]** | Email update subject to UNIQUE constraint (pre-check before UPDATE) |
|
||||||
| **[RG-4 — PIN + audit + allowlist]** | Editing an account (incl. `role_id`, the privilege-escalation vector) is sensitive: per-staff PIN (RG-T13) + one `audit_log` row (RG-T14), `action_code='user.update'`, `entity_type='user'`, `entity_id=:id`, `details` listing changed field names (not values, no PII). Only the allowlisted columns are bound (RG-T16): `first_name`, `last_name`, `email`, `role_id`, `is_active` (+ optional password rehash). |
|
| **[POST-1]** | `user` updated, `updated_at` refreshed |
|
||||||
| **[POST-1]** | `user` updated, `updated_at` refreshed; one `audit_log` row recorded |
|
|
||||||
| **[OUT-1]** | Redirect with success message |
|
| **[OUT-1]** | Redirect with success message |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -486,8 +465,7 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[PRE-2]** | Actor is not targeting their own account (`$targetUserId !== $currentUserId`) |
|
| **[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-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 |
|
| **[RG-2]** | The user's potentially active session is invalidated on next request: middleware checks `user.is_active = 1` on each authenticated request |
|
||||||
| **[RG-3 — PIN + audit]** | Sensitive action: per-staff PIN (RG-T13) + one `audit_log` row (RG-T14), `action_code='user.deactivate'`, `entity_type='user'`, `entity_id=:id`. |
|
| **[POST-1]** | `user.is_active = 0`; user cannot log in; history remains intact |
|
||||||
| **[POST-1]** | `user.is_active = 0`; user cannot log in; history remains intact; one `audit_log` row recorded |
|
|
||||||
| **[OUT-1]** | Redirect with success message |
|
| **[OUT-1]** | Redirect with success message |
|
||||||
| **[ERR-1]** | Self-deactivation attempt: HTTP 403, `{error: {code: "SELF_DEACTIVATION_FORBIDDEN"}}` |
|
| **[ERR-1]** | Self-deactivation attempt: HTTP 403, `{error: {code: "SELF_DEACTIVATION_FORBIDDEN"}}` |
|
||||||
|
|
||||||
|
|
@ -507,31 +485,11 @@ These rules apply to multiple operations and are centralised here to avoid repet
|
||||||
| **[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-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-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). |
|
| **[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). |
|
||||||
| **[RG-6 — PIN + audit change-log]** | RBAC changes are high-impact (privilege escalation): per-staff PIN (RG-T13) + one `audit_log` row (RG-T14) per change, `action_code='role.manage'`, `entity_type='role'`, `entity_id=:role_id`. Because permissions are rewritten delete-and-reinsert (RG-1), the `details` JSON records the **diff** — permission codes added and removed — computed before the rewrite, so the trail shows exactly which capabilities a role gained or lost and who granted them. |
|
| **[POST-1]** | `role_permission` reflects exactly the selected permissions for this role |
|
||||||
| **[POST-1]** | `role_permission` reflects exactly the selected permissions for this role; one `audit_log` row recorded with the permission diff |
|
|
||||||
| **[OUT-1]** | Redirect with success message |
|
| **[OUT-1]** | Redirect with success message |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 10.5 ERASE_USER_PII (RGPD anonymisation)
|
|
||||||
|
|
||||||
**Security-by-design operation (no v0.1 / v0.2 MCT predecessor). Honours the RGPD right to
|
|
||||||
erasure (Cr 3.d) without breaking referential integrity or the audit trail (dict. note 13).**
|
|
||||||
|
|
||||||
| Tag | Content |
|
|
||||||
|-----|---------|
|
|
||||||
| **[PRE-1]** | Actor authenticated, holds permission `user.update` (erasure is an admin operation) |
|
|
||||||
| **[PRE-2]** | Per-staff PIN verified (RG-T13) — sensitive action |
|
|
||||||
| **[PRE-3]** | Target `user.id` exists and `anonymized_at IS NULL` (not already anonymised) |
|
|
||||||
| **[RG-1 — anonymise, not delete]** | In one transaction: `UPDATE user SET email = CONCAT('anon-', id, '@wakdo.invalid'), first_name = '', last_name = '', password_hash = '', pin_hash = NULL, password_reset_token_hash = NULL, is_active = 0, anonymized_at = NOW() WHERE id = :id`. The placeholder domain is RFC 2606 reserved (`.invalid`), keeps `email` UNIQUE and non-identifying. |
|
|
||||||
| **[RG-2 — preserve links]** | The row persists, so FKs pointing at it (`stock_movement.user_id`, `customer_order.acting_user_id`, `audit_log.actor_user_id`) stay valid and now resolve to an anonymised principal. Accountability for past actions is preserved in form (who-as-id) without retaining PII. |
|
|
||||||
| **[RG-3 — audit]** | One `audit_log` row (RG-T14): `action_code='user.erase_pii'`, `entity_type='user'`, `entity_id=:id`. The `summary`/`details` record the erasure event and its legal basis, not the erased values. |
|
|
||||||
| **[POST-1]** | `user` row anonymised: PII fields cleared/placeholdered, credentials invalidated, `anonymized_at` set, `is_active = 0`. Referential links intact. |
|
|
||||||
| **[OUT-1]** | Confirmation; the user disappears from active lists, remains as an anonymised tombstone in history. |
|
|
||||||
| **[ERR-1]** | Already anonymised: HTTP 409, `{error: {code: "ALREADY_ANONYMISED"}}` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Domain 9 — Stats and KPI
|
## 11. Domain 9 — Stats and KPI
|
||||||
|
|
||||||
### 11.1 READ_STATS
|
### 11.1 READ_STATS
|
||||||
|
|
@ -561,21 +519,17 @@ erasure (Cr 3.d) without breaking referential integrity or the audit trail (dict
|
||||||
|-----|---------|
|
|-----|---------|
|
||||||
| **[PRE-1]** | Login form submitted with email and password |
|
| **[PRE-1]** | Login form submitted with email and password |
|
||||||
| **[PRE-2]** | CSRF token of the form is valid (anti-CSRF protection) |
|
| **[PRE-2]** | CSRF token of the form is valid (anti-CSRF protection) |
|
||||||
| **[PRE-3 — throttle gate]** | If the account is in a throttling window (`user.lockout_until IS NOT NULL AND lockout_until > NOW()`), reject with the generic error before any password check. Throttling is also keyed per source IP via the `login_throttle` table: if a row exists for the source IP with `lockout_until IS NOT NULL AND lockout_until > NOW()`, reject with the same generic error, so distributed attempts on many accounts are slowed too. |
|
|
||||||
| **[RG-1]** | Lookup: `SELECT * FROM user WHERE email = :email AND is_active = 1 LIMIT 1` |
|
| **[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). To keep timing comparable when the email is unknown, a dummy `password_verify` against a fixed decoy hash is run. |
|
| **[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-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-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-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-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) |
|
| **[RG-7]** | Redirect target is `role.default_route` (dynamic; no hardcoded role name in routing logic) |
|
||||||
| **[RG-8 — failure handling, degressive backoff]** | On a failed verification, the per-account counter on `user`: `UPDATE user SET failed_login_attempts = failed_login_attempts + 1, last_failed_login_at = NOW()`, and once a threshold is reached (suggestion: 5) set `lockout_until = NOW() + INTERVAL (base * 2^(attempts - threshold)) SECOND`, capped (suggestion: cap a few minutes). In the same step, the per-IP dimension is recorded in the `login_throttle` table: upsert the row keyed on `ip_address` (insert if absent, else increment `failed_attempts`; reset the window when expired via `window_started_at`), update `last_attempt_at = NOW()`, and once the IP threshold is reached set `lockout_until` with the same degressive backoff. This is a degressive backoff, not an indefinite lock — it slows brute force without letting a fat-finger streak deny service to a kitchen mid-shift. Write one `audit_log` row (`action_code='auth.login_failed'`, `actor_user_id` if the email resolved, else NULL). |
|
| **[POST-1]** | PHP session open with `user_id` and `role_id`; `user.last_login_at` updated |
|
||||||
| **[RG-9 — success reset]** | On success, reset the per-account counter `failed_login_attempts = 0`, clear `lockout_until = NULL`, and also clear the per-IP `login_throttle` row for the source IP (reset `failed_attempts = 0`, `lockout_until = NULL`, restart `window_started_at`), then write one `audit_log` row (`action_code='auth.login_success'`, `actor_user_id`, `actor_role_id`). |
|
|
||||||
| **[POST-1]** | PHP session open with `user_id` and `role_id`; `user.last_login_at` updated; `failed_login_attempts` reset |
|
|
||||||
| **[OUT-1]** | Redirect to `role.default_route` |
|
| **[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); failure counter incremented (RG-8) |
|
| **[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 |
|
| **[ERR-2]** | Invalid CSRF token: HTTP 403 |
|
||||||
| **[ERR-3]** | Account in throttling window (PRE-3): same generic message; the attempt does not reveal that the account exists or is locked |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -594,21 +548,7 @@ erasure (Cr 3.d) without breaking referential integrity or the audit trail (dict
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 12.3 RESET_PASSWORD
|
## 13. Automated treatments — Crons (outside user interactions)
|
||||||
|
|
||||||
**Security-by-design operation (no v0.1 predecessor). Two phases: request, then confirm.**
|
|
||||||
|
|
||||||
| Tag | Content |
|
|
||||||
|-----|---------|
|
|
||||||
| **[PRE-1]** | Request phase: a `user` submits the "forgot password" form with an email; CSRF token valid |
|
|
||||||
| **[RG-1 — request, enumeration-safe]** | Look up the email. The same neutral response ("if the account exists, an email has been sent") is returned whether or not the email exists, to avoid account enumeration. |
|
|
||||||
| **[RG-2 — token generation]** | If the email resolves to an active user: generate a cryptographically random token (e.g. 32 bytes from a CSPRNG); store its **hash** in `password_reset_token_hash` and `password_reset_expires_at = NOW() + INTERVAL 1 HOUR`. The **raw** token is sent once in the reset link (not stored in clear). |
|
|
||||||
| **[PRE-2]** | Confirm phase: the user opens the reset link with the raw token and submits a new password; CSRF token valid |
|
|
||||||
| **[RG-3 — confirm]** | Hash the submitted token and match it against `password_reset_token_hash` where `password_reset_expires_at > NOW()`. On match: `password_hash = password_hash($new, PASSWORD_ARGON2ID)` (min length 8), then clear `password_reset_token_hash = NULL` and `password_reset_expires_at = NULL`, and reset `failed_login_attempts = 0`, `lockout_until = NULL`. One-time use. |
|
|
||||||
| **[RG-4 — audit]** | Write one `audit_log` row (RG-T14), `action_code='auth.password_reset'`, `actor_user_id = :id`. |
|
|
||||||
| **[POST-1]** | Password replaced with a new argon2id hash; reset token consumed and cleared |
|
|
||||||
| **[OUT-1]** | Confirmation; redirect to login |
|
|
||||||
| **[ERR-1]** | Invalid or expired token: generic message inviting a new reset request (no detail on which condition failed) |
|
|
||||||
|
|
||||||
These treatments are executed by the `wakdo-cron` service container in the maintenance
|
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
|
window 01:30-09:30 (outside active service). They are outside the MCT scope (technical
|
||||||
|
|
@ -641,24 +581,6 @@ treatments, no user trigger) but are documented here for consistency with PROJEC
|
||||||
| **[RG-2]** | Retention: keep the last 7 dumps; delete older ones |
|
| **[RG-2]** | Retention: keep the last 7 dumps; delete older ones |
|
||||||
| **[POST-1]** | SQL dump available for restoration |
|
| **[POST-1]** | SQL dump available for restoration |
|
||||||
|
|
||||||
### 13.4 Audit log retention purge (cron daily)
|
|
||||||
|
|
||||||
| Tag | Content |
|
|
||||||
|-----|---------|
|
|
||||||
| **[TRIGGER]** | Cron: `15 4 * * *` (maintenance window) |
|
|
||||||
| **[RG-1]** | `DELETE FROM audit_log WHERE created_at < NOW() - INTERVAL :retention_months MONTH` (suggestion: 12 months, legitimate-interest / fiscal traceability — configurable in `.env`). |
|
|
||||||
| **[RG-2]** | The window is decoupled from user PII lifecycle: anonymisation (10.5) removes PII immediately on request, while the audit trail ages out on its own schedule (dict. note 13). |
|
|
||||||
| **[POST-1]** | `audit_log` rows older than the retention window removed; recent accountability preserved. |
|
|
||||||
|
|
||||||
### 13.5 login_throttle purge (cron daily)
|
|
||||||
|
|
||||||
| Tag | Content |
|
|
||||||
|-----|---------|
|
|
||||||
| **[TRIGGER]** | Cron: `45 4 * * *` (maintenance window) |
|
|
||||||
| **[RG-1]** | `DELETE FROM login_throttle WHERE (lockout_until IS NULL OR lockout_until < NOW()) AND last_attempt_at < NOW() - INTERVAL 24 HOUR` — purge rows with no active lockout whose last failed attempt is older than 24h. |
|
|
||||||
| **[RG-2]** | Rows still serving an active lockout are retained; the per-IP counter (S1) is bounded by this purge so the table does not grow unbounded from one-off attempts. |
|
|
||||||
| **[POST-1]** | Stale `login_throttle` rows removed; active throttles and recent activity preserved. |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. State machine — consistency recap (MLT)
|
## 14. State machine — consistency recap (MLT)
|
||||||
|
|
|
||||||
|
|
@ -1,232 +0,0 @@
|
||||||
# Diagramme de sequence securite - Annulation de commande avec PIN (CANCEL_ORDER)
|
|
||||||
|
|
||||||
**Phase UML** : P1 - Conception, complement UML (passe security-by-design)
|
|
||||||
**Statut** : v0.2 - flux sensible PIN-gate + audit_log (controle anti-fraude interne)
|
|
||||||
**Date** : 2026-06-12
|
|
||||||
**Branche** : `feat/p1-conception`
|
|
||||||
**Auteur methodologie** : BYAN
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Objet du document
|
|
||||||
|
|
||||||
Ce document decrit le **flux temporel securise** de l'annulation d'une commande
|
|
||||||
en back-office (`CANCEL_ORDER`). L'annulation est une action de **manipulation
|
|
||||||
d'argent** : annuler une commande deja `paid` peut servir a masquer un
|
|
||||||
detournement d'especes (l'equipier encaisse, annule, garde le cash). Le flux
|
|
||||||
ci-dessous materialise le controle qui adresse ce risque : une **re-authentification
|
|
||||||
par PIN par equipier** (`RG-T13`) avant l'execution, et l'ecriture d'une ligne
|
|
||||||
**`audit_log` immuable** dans la meme transaction que l'effet (`RG-T14`), de sorte
|
|
||||||
que chaque annulation est rattachee a une personne meme sur un poste partage.
|
|
||||||
|
|
||||||
Le diagramme reste au niveau conceptuel / logique. Il nomme les echanges entre
|
|
||||||
participants sans detailler l'implementation PHP ni le SQL exact. Il complete
|
|
||||||
l'operation `CANCEL_ORDER` du `docs/merise/mlt.md` (7.1), la transition T5 de
|
|
||||||
`docs/uml/state-commande.md` (`paid -> cancelled`, re-credit du stock) et le cas
|
|
||||||
d'utilisation "Annuler une commande" de `docs/uml/use-cases.md` (UC13).
|
|
||||||
|
|
||||||
**Sources** :
|
|
||||||
- `docs/merise/mlt.md` 7.1 `CANCEL_ORDER` (PRE-1..PRE-3, RG-1..RG-6, POST, ERR)
|
|
||||||
- `docs/merise/mlt.md` section 2 : `RG-T13` (PIN sensible), `RG-T14` (audit_log), `RG-T11` (re-credit dans la meme transaction), `RG-T07` (garde de concurrence), `RG-T08` (transaction atomique)
|
|
||||||
- `docs/merise/dictionary.md` 3.14 (`user.pin_hash`, argon2id) et 3.20 (`audit_log`)
|
|
||||||
- `docs/uml/state-commande.md` T5 (`paid -> cancelled`, `stock_movement` type `cancellation`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Participants
|
|
||||||
|
|
||||||
| Participant | Role | Couche |
|
|
||||||
|---|---|---|
|
|
||||||
| **Equipier** | Counter / Drive / Admin titulaire de `order.cancel`, sur poste partage | Acteur |
|
|
||||||
| **Admin UI** | Interface back-office (Bloc 3, formulaire d'annulation + saisie PIN) | Presentation |
|
|
||||||
| **Controleur** | Back-end REST sous `/api/*` (Bloc 2), orchestre la transaction | Application |
|
|
||||||
| **Verif PIN** | Service de verification du PIN (`password_verify` argon2id) | Application |
|
|
||||||
| **BDD** | Base de donnees MariaDB | Persistance |
|
|
||||||
|
|
||||||
La session est **partagee par poste de travail** pour le routine 95% ; le PIN
|
|
||||||
re-introduit une attribution individuelle sur le sous-ensemble sensible
|
|
||||||
(`mlt.md` `RG-T13`). Le PIN n'est pas une session : il est verifie a chaque action
|
|
||||||
sensible et sert a capturer le `user_id` qui sera ecrit dans `audit_log`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Diagramme de sequence
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
actor Equipier
|
|
||||||
participant AdminUI as Admin UI
|
|
||||||
participant Ctrl as Controleur
|
|
||||||
participant PIN as Verif PIN
|
|
||||||
participant BDD
|
|
||||||
|
|
||||||
Note over Equipier,BDD: Phase 1 - Demande et controle de permission
|
|
||||||
|
|
||||||
Equipier->>AdminUI: demander l'annulation d'une commande
|
|
||||||
AdminUI->>Ctrl: POST /api/orders/{id}/cancel
|
|
||||||
Ctrl->>Ctrl: verifier session active + is_active = 1 (RG-T02)
|
|
||||||
Ctrl->>BDD: lire role_permission (permission order.cancel ?) (RG-T03, PRE-1)
|
|
||||||
BDD-->>Ctrl: permission presente
|
|
||||||
Ctrl->>BDD: lire customer_order (existe ? statut ?) (PRE-2, PRE-3)
|
|
||||||
BDD-->>Ctrl: status courant
|
|
||||||
|
|
||||||
alt status hors ['pending_payment','paid'] (delivered ou cancelled)
|
|
||||||
Ctrl-->>AdminUI: 422 CANNOT_CANCEL_IN_STATE {current_status}
|
|
||||||
AdminUI-->>Equipier: refus, aucun changement d'etat (ERR-1)
|
|
||||||
else status annulable
|
|
||||||
Note over Equipier,BDD: Phase 2 - Re-authentification PIN (RG-T13)
|
|
||||||
|
|
||||||
Ctrl-->>AdminUI: demander la saisie du PIN
|
|
||||||
Equipier->>AdminUI: saisir le PIN
|
|
||||||
AdminUI->>Ctrl: soumettre le PIN (re-auth action sensible)
|
|
||||||
Ctrl->>PIN: verifier le PIN soumis
|
|
||||||
PIN->>BDD: lire user.pin_hash de l'equipier
|
|
||||||
BDD-->>PIN: pin_hash (argon2id)
|
|
||||||
PIN->>PIN: password_verify(pin, pin_hash)
|
|
||||||
|
|
||||||
alt PIN incorrect
|
|
||||||
PIN-->>Ctrl: echec
|
|
||||||
Ctrl-->>AdminUI: refus du PIN, action rejetee
|
|
||||||
AdminUI-->>Equipier: PIN incorrect, aucun changement d'etat
|
|
||||||
else PIN correct
|
|
||||||
PIN-->>Ctrl: succes, acting user_id capture (RG-T13)
|
|
||||||
|
|
||||||
Note over Equipier,BDD: Phase 3 - Transaction atomique (RG-T08, RG-T11)
|
|
||||||
|
|
||||||
Ctrl->>BDD: BEGIN transaction
|
|
||||||
Ctrl->>BDD: UPDATE customer_order SET status = 'cancelled', cancelled_at = NOW() WHERE id = :id AND status IN ('pending_payment','paid') (RG-1, RG-T07)
|
|
||||||
BDD-->>Ctrl: lignes affectees
|
|
||||||
|
|
||||||
alt 0 ligne affectee (annulation concurrente)
|
|
||||||
Ctrl->>BDD: ROLLBACK
|
|
||||||
Ctrl-->>AdminUI: 409 INVALID_TRANSITION (ERR-2)
|
|
||||||
AdminUI-->>Equipier: deja annulee par un autre poste
|
|
||||||
else 1 ligne affectee
|
|
||||||
opt statut anterieur = paid (re-credit du stock)
|
|
||||||
Ctrl->>BDD: UPDATE ingredient SET stock_quantity = stock_quantity + :units (par ingredient consomme) (RG-3)
|
|
||||||
Ctrl->>BDD: INSERT stock_movement (type cancellation, delta +units, order_id, user_id de l'equipier) (RG-3)
|
|
||||||
end
|
|
||||||
Ctrl->>BDD: INSERT audit_log (action_code order.cancel, actor_user_id, actor_role_id, entity_type customer_order, entity_id, summary [statut anterieur + montant re-credite]) (RG-6, RG-T14)
|
|
||||||
Ctrl->>BDD: COMMIT
|
|
||||||
BDD-->>Ctrl: transaction validee
|
|
||||||
Ctrl-->>AdminUI: 200 annulation confirmee (OUT-1)
|
|
||||||
AdminUI-->>Equipier: commande annulee, trace enregistree
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Notes de modelisation : chaque pas et sa regle
|
|
||||||
|
|
||||||
Le tableau ci-dessous mappe chaque interaction du diagramme a la regle
|
|
||||||
`mlt.md` 7.1 ou a la regle transverse correspondante, et a l'entite ecrite.
|
|
||||||
|
|
||||||
| # | Interaction | Regle (mlt.md) | Entite ecrite / lue |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | Verifier session active + `is_active = 1` | `RG-T02` | `user` (lecture) |
|
|
||||||
| 2 | Verifier `order.cancel` via `role_permission` | `RG-T03`, 7.1 PRE-1 | `role_permission` (lecture) |
|
|
||||||
| 3 | Charger la commande et lire son `status` | 7.1 PRE-2, PRE-3 | `customer_order` (lecture) |
|
|
||||||
| 4 | Bloquer si `status` est `delivered` ou `cancelled` | 7.1 ERR-1 | aucune ecriture (HTTP 422) |
|
|
||||||
| 5 | Demander + verifier le PIN (`password_verify` argon2id) | `RG-T13`, 7.1 RG-6 | `user.pin_hash` (lecture) |
|
|
||||||
| 6 | Rejeter si PIN incorrect, sans changement d'etat | `RG-T13` | aucune ecriture |
|
|
||||||
| 7 | Capturer l'`acting user_id` pour l'audit | `RG-T13` | (en memoire, sert aux pas 11-12) |
|
|
||||||
| 8 | `BEGIN` transaction | `RG-T08` | transaction |
|
|
||||||
| 9 | `UPDATE customer_order SET status='cancelled'` avec garde `AND status IN (...)` | 7.1 RG-1, `RG-T07` | `customer_order` (ecriture) |
|
|
||||||
| 10 | `ROLLBACK` + 409 si 0 ligne affectee (concurrence) | 7.1 ERR-2, `RG-T07` | aucune ecriture nette |
|
|
||||||
| 11 | Re-credit conditionnel du stock si statut anterieur `paid` | 7.1 RG-3, `RG-T11` | `ingredient`, `stock_movement` (type `cancellation`) |
|
|
||||||
| 12 | `INSERT audit_log` dans la meme transaction | 7.1 RG-6, `RG-T14` | `audit_log` (ecriture) |
|
|
||||||
| 13 | `COMMIT` (tout ou rien) | `RG-T08`, `RG-T11` | transaction |
|
|
||||||
| 14 | Reponse 200 de confirmation | 7.1 OUT-1 | aucune ecriture |
|
|
||||||
|
|
||||||
### 4.1 Re-credit conditionnel du stock (`RG-T11`)
|
|
||||||
|
|
||||||
Le re-credit ne s'applique que si la commande etait au statut `paid` avant
|
|
||||||
l'annulation (7.1 RG-3). Une commande `pending_payment` n'avait pas encore
|
|
||||||
decremente le stock (le decrement a lieu a la transition `paid`), donc il n'y a
|
|
||||||
rien a re-crediter. Pour chaque `order_item` d'une commande `paid`, les unites
|
|
||||||
consommees sont recalculees (format `normal`/`maxi`, ajustees par les
|
|
||||||
`order_item_modifier`), `ingredient.stock_quantity` est re-incremente et un
|
|
||||||
`stock_movement` de type `cancellation` est insere. `RG-T11` garantit que ce
|
|
||||||
re-credit et l'`UPDATE` du statut sont dans la **meme transaction** : il n'y a pas
|
|
||||||
de decrement orphelin si une etape echoue.
|
|
||||||
|
|
||||||
### 4.2 Garde de concurrence (`RG-T07`)
|
|
||||||
|
|
||||||
L'`UPDATE` porte la clause `AND status IN ('pending_payment','paid')`. Si deux
|
|
||||||
postes tentent d'annuler la meme commande au meme instant, seul le premier
|
|
||||||
obtient une ligne affectee ; le second recoit 0 ligne et le controleur repond
|
|
||||||
409 `INVALID_TRANSITION` apres `ROLLBACK` (7.1 ERR-2). Cette garde optimiste
|
|
||||||
reduit le risque d'une double annulation et d'un double re-credit du stock.
|
|
||||||
|
|
||||||
### 4.3 PIN distinct de la session (`RG-T13`)
|
|
||||||
|
|
||||||
La session reste **partagee par poste** pour le flux routine. Le PIN est verifie
|
|
||||||
a chaque action du sous-ensemble sensible (annulation, prix/VAT, RBAC, gestion
|
|
||||||
utilisateur, correction d'inventaire), et c'est lui qui fournit l'`actor_user_id`
|
|
||||||
ecrit dans `audit_log`. Le `pin_hash` est un hash argon2id (`dictionary.md` 3.14),
|
|
||||||
compare via `password_verify` ; il fait partie des champs RESTRICTED tenus hors
|
|
||||||
des logs et des reponses API.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Menace adressee : repudiation et detournement d'especes
|
|
||||||
|
|
||||||
L'annulation d'une commande `paid` est le geste qui permet le schema de fraude
|
|
||||||
"encaisser puis annuler pour garder le cash" (insider cash-skim). Sans controle,
|
|
||||||
sur un poste a session partagee, une annulation ne serait rattachee a personne :
|
|
||||||
l'auteur pourrait nier l'avoir faite (repudiation). Le flux ci-dessus reduit le
|
|
||||||
risque de ce schema en combinant deux mecanismes concrets :
|
|
||||||
|
|
||||||
- **PIN par equipier (`RG-T13`)** : l'annulation exige une re-authentification
|
|
||||||
individuelle. Sur un poste partage, cela rattache l'acte a une personne et non
|
|
||||||
au seul poste. Le PIN tend a dissuader l'usage opportuniste d'une session
|
|
||||||
laissee ouverte par un collegue.
|
|
||||||
- **`audit_log` immuable (`RG-T14`)** : chaque annulation ecrit une ligne
|
|
||||||
`audit_log` (`action_code = order.cancel`, `actor_user_id`, `actor_role_id`,
|
|
||||||
`entity_type`, `entity_id`, `summary` avec le statut anterieur et le montant
|
|
||||||
re-credite) dans la **meme transaction** que l'`UPDATE` du statut. La table
|
|
||||||
n'accepte ni `UPDATE` ni `DELETE` au niveau applicatif (`dictionary.md` 3.20).
|
|
||||||
Une annulation ne peut donc pas exister sans sa trace, et la trace ne peut pas
|
|
||||||
etre effacee par l'auteur.
|
|
||||||
|
|
||||||
L'effet combine : un pic d'annulations rattachees a un meme `actor_user_id`
|
|
||||||
devient visible et opposable lors d'une revue. Ceci ne supprime pas le risque,
|
|
||||||
mais le **reduit** en transformant un acte anonyme et niable en un acte attribue
|
|
||||||
et trace. La residualite (collusion, partage de PIN) releve de controles
|
|
||||||
organisationnels hors du modele de donnees.
|
|
||||||
|
|
||||||
> Note : `audit_log` enregistre des **noms de champs** et un `summary`
|
|
||||||
> non-personnel (`details` stocke les noms de champs modifies, pas de PII),
|
|
||||||
> conformement a `RG-T14` et a la classification de `PROJECT_CONTEXT.md` section 19.
|
|
||||||
> L'attribution `stock_movement.user_id` du re-credit complete la trace cote stock
|
|
||||||
> sans double journalisation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Coherence avec les autres livrables
|
|
||||||
|
|
||||||
| Verification | Resultat |
|
|
||||||
|---|---|
|
|
||||||
| Statuts annulables coherents avec `state-commande.md` | Oui : `pending_payment` et `paid` (T3, T5) ; `delivered` non annulable (7.1 ERR-1) |
|
|
||||||
| Transition `paid -> cancelled` avec re-credit | Couverte par T5 et 7.1 RG-3 (`stock_movement` type `cancellation`) |
|
|
||||||
| Entites ecrites presentes au dictionnaire | `customer_order` (3.10), `ingredient`, `stock_movement`, `audit_log` (3.20) |
|
|
||||||
| Regle PIN appliquee | `RG-T13` (sous-ensemble sensible inclut 7.1) ; `user.pin_hash` (3.14) |
|
|
||||||
| Regle audit appliquee | `RG-T14` ; colonnes conformes a `audit_log` (3.20) |
|
|
||||||
| Atomicite re-credit + statut + audit | `RG-T08` + `RG-T11` (une transaction, `COMMIT` / `ROLLBACK`) |
|
|
||||||
| Codes d'erreur | 422 `CANNOT_CANCEL_IN_STATE` (ERR-1), 409 `INVALID_TRANSITION` (ERR-2) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Arbitrage tranche
|
|
||||||
|
|
||||||
Le flux retient la re-authentification **par PIN** plutot qu'une re-saisie du mot
|
|
||||||
de passe complet : le PIN couvre le sous-ensemble sensible sans casser le routine
|
|
||||||
95% a session partagee (`RG-T13`), tout en fournissant l'attribution
|
|
||||||
individuelle. L'`audit_log` est ecrit dans la **meme transaction** que l'effet
|
|
||||||
(`RG-T14` + `RG-T08`) : une annulation sans trace ne peut pas etre committee. Le
|
|
||||||
re-credit du stock est conditionnel au statut anterieur `paid` (`RG-T11`), ce qui
|
|
||||||
ecarte un re-credit indu sur une commande `pending_payment` qui n'a pas ete
|
|
||||||
decrementee. Les codes d'erreur (`CANNOT_CANCEL_IN_STATE`, `INVALID_TRANSITION`)
|
|
||||||
reprennent ceux de `mlt.md` 7.1, sans en inventer de nouveaux.
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Diagramme de sequence - Passer une commande (borne client)
|
# Diagramme de sequence - Passer une commande (borne client)
|
||||||
|
|
||||||
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
||||||
**Statut** : v0.2 - prod-like, creation atomique (create + pay)
|
**Statut** : v0.1
|
||||||
**Date** : 2026-06-11
|
**Date** : 2026-05-21
|
||||||
**Branche** : `feat/p1-conception`
|
**Branche** : `feat/p1-conception`
|
||||||
**Auteur methodologie** : BYAN
|
**Auteur methodologie** : BYAN
|
||||||
|
|
||||||
|
|
@ -12,24 +12,18 @@
|
||||||
|
|
||||||
Ce document decrit le **flux temporel** du parcours "passer une commande" cote
|
Ce document decrit le **flux temporel** du parcours "passer une commande" cote
|
||||||
**Client sur la borne kiosk** : navigation dans les categories, selection d'un
|
**Client sur la borne kiosk** : navigation dans les categories, selection d'un
|
||||||
produit ou composition d'un menu (slots + format Normal/Maxi + modifiers
|
produit ou composition d'un menu, gestion du panier, validation avec saisie du
|
||||||
d'ingredients), gestion du panier, validation avec saisie du numero de retrait,
|
numero de retrait, paiement, puis confirmation.
|
||||||
et confirmation. Dans le modele v0.2, la **creation et le paiement sont
|
|
||||||
atomiques** : un seul appel `POST /api/orders` cree la commande, la fait passer a
|
|
||||||
`paid`, decremente le stock et journalise les mouvements, dans une meme
|
|
||||||
transaction.
|
|
||||||
|
|
||||||
Le diagramme reste au niveau conceptuel / logique. Il nomme les echanges entre
|
Le diagramme reste au niveau **conceptuel / logique**. Il nomme les echanges
|
||||||
participants sans detailler l'implementation PHP ni le SQL exact. Il complete le
|
entre participants sans detailler l'implementation PHP (controllers, models)
|
||||||
cas d'utilisation "Passer une commande" de `docs/uml/use-cases.md` (4.1), la
|
ni le SQL exact. Il complete le cas d'utilisation "Passer une commande" de
|
||||||
machine a etats de `docs/uml/state-commande.md` (T1/T2) et l'operation
|
`docs/uml/use-cases.md` et la machine a etats de `docs/uml/state-commande.md`.
|
||||||
`CREATE_ORDER` du `docs/merise/mct.md` (3.3).
|
|
||||||
|
|
||||||
**Sources** :
|
**Sources** :
|
||||||
- `docs/PROJECT_CONTEXT.md` section 2 (processus metier), section 7 (endpoints API)
|
- `docs/PROJECT_CONTEXT.md` section 2 (processus metier), section 7 (endpoints API)
|
||||||
- `docs/merise/dictionary.md` 3.10-3.13 (`customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`)
|
- `docs/merise/dictionary.md` (`commande`, `ligne_commande`, `menu`, `produit`)
|
||||||
- `docs/merise/mct.md` 3.3 CREATE_ORDER (transaction, snapshots, decrement stock)
|
- `docs/uml/state-commande.md` (transitions `pending_payment -> paid`)
|
||||||
- `docs/uml/state-commande.md` (transitions T1/T2, atomicite `pending_payment -> paid`)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -77,18 +71,16 @@ sequenceDiagram
|
||||||
|
|
||||||
alt Produit a la carte
|
alt Produit a la carte
|
||||||
Client->>Borne: selectionner un produit
|
Client->>Borne: selectionner un produit
|
||||||
opt Personnaliser les ingredients
|
Client->>Borne: regler taille / options
|
||||||
Client->>Borne: retirer / ajouter un ingredient
|
|
||||||
end
|
|
||||||
Borne->>Borne: ajouter la ligne au panier local
|
Borne->>Borne: ajouter la ligne au panier local
|
||||||
else Composition d'un menu
|
else Composition d'un menu
|
||||||
Client->>Borne: selectionner un menu
|
Client->>Borne: selectionner un menu
|
||||||
Borne->>API: GET /api/menus (slots + options eligibles)
|
Borne->>API: GET /api/menus (composition du menu)
|
||||||
API->>BDD: lire menu, menu_slot, menu_slot_option
|
API->>BDD: lire menu et composition
|
||||||
BDD-->>API: menu + slots + options
|
BDD-->>API: menu + produits par role
|
||||||
API-->>Borne: composition (JSON)
|
API-->>Borne: composition (JSON)
|
||||||
Borne-->>Client: afficher les slots (boisson, accompagnement, sauce) + format Normal/Maxi
|
Borne-->>Client: afficher les choix par slot (burger, accompagnement, boisson, sauce)
|
||||||
Client->>Borne: choisir chaque slot + format + modifiers du burger
|
Client->>Borne: choisir chaque composant + tailles
|
||||||
Borne->>Borne: ajouter la ligne menu au panier local
|
Borne->>Borne: ajouter la ligne menu au panier local
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -102,35 +94,36 @@ sequenceDiagram
|
||||||
Borne-->>Client: panier mis a jour
|
Borne-->>Client: panier mis a jour
|
||||||
end
|
end
|
||||||
|
|
||||||
Note over Client,BDD: Phase 4 - Validation, saisie du numero, creation atomique (create + pay)
|
Note over Client,BDD: Phase 4 - Validation du panier et saisie du numero
|
||||||
|
|
||||||
Client->>Borne: valider la commande
|
Client->>Borne: valider la commande
|
||||||
Client->>Borne: saisir le numero de retrait
|
Client->>Borne: saisir le numero de retrait
|
||||||
Borne->>Borne: valider le panier (au moins 1 ligne, numero non vide)
|
Borne->>Borne: valider le panier (au moins 1 ligne)
|
||||||
Borne->>API: POST /api/orders (lignes + selections + modifiers + service_mode + numero)
|
Borne->>API: POST /api/orders (lignes + mode_consommation + numero)
|
||||||
|
|
||||||
API->>API: recalculer les totaux cote serveur (HT / TVA / TTC, taux par produit)
|
API->>API: recalculer les totaux cote serveur
|
||||||
API->>BDD: BEGIN transaction
|
API->>BDD: creer la commande (statut pending_payment)
|
||||||
API->>BDD: INSERT customer_order (status pending_payment, source kiosk)
|
API->>BDD: creer les lignes (snapshot libelle + prix)
|
||||||
API->>BDD: INSERT order_item (snapshot libelle + prix + vat_rate)
|
BDD-->>API: commande persistee {id, numero, statut: pending_payment}
|
||||||
API->>BDD: INSERT order_item_selection (par slot de menu rempli)
|
API-->>Borne: 201 Created {id, numero, statut: pending_payment, total}
|
||||||
API->>BDD: INSERT order_item_modifier (par modification d'ingredient)
|
Borne-->>Client: afficher le total a regler
|
||||||
API->>BDD: UPDATE ingredient.stock_quantity (decrement, ajuste par modifiers)
|
|
||||||
API->>BDD: INSERT stock_movement (type sale, par unite consommee)
|
|
||||||
API->>BDD: UPDATE customer_order status -> paid, paid_at = NOW()
|
|
||||||
API->>BDD: COMMIT
|
|
||||||
BDD-->>API: commande persistee {id, order_number, status: paid}
|
|
||||||
|
|
||||||
Note over Client,BDD: Phase 5 - Confirmation
|
Note over Client,BDD: Phase 5 - Paiement (pending_payment -> paid)
|
||||||
|
|
||||||
API-->>Borne: 201 Created {id, order_number, status: paid, total_ttc}
|
Client->>Borne: payer la commande
|
||||||
Borne-->>Client: ecran de confirmation avec le numero de retrait
|
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
|
Note over Client,BDD: Cas d'erreur
|
||||||
|
|
||||||
alt Panier vide, produit indisponible ou donnees invalides
|
alt Panier vide ou donnees invalides
|
||||||
API->>BDD: ROLLBACK (si transaction entamee)
|
API-->>Borne: 4xx {error: code, message}
|
||||||
API-->>Borne: 4xx {error: {code, message}}
|
|
||||||
Borne-->>Client: message d'erreur, retour au panier
|
Borne-->>Client: message d'erreur, retour au panier
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
@ -139,31 +132,27 @@ sequenceDiagram
|
||||||
|
|
||||||
## 4. Notes de modelisation
|
## 4. Notes de modelisation
|
||||||
|
|
||||||
### 4.1 Recalcul des totaux cote serveur (controle de securite)
|
### 4.1 Recalcul des totaux cote serveur
|
||||||
|
|
||||||
La Borne affiche un total **provisoire** calcule localement pour l'experience
|
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
|
utilisateur. L'API recalcule les totaux a la reception du `POST /api/orders` a
|
||||||
partir des prix en base (HT, TVA ligne par ligne via `vat_rate`, TTC), puis fige
|
partir des prix en base, puis fige les snapshots
|
||||||
les snapshots (`unit_price_cents_snapshot`, `vat_rate_snapshot`,
|
(`prix_unitaire_ttc_cents_snapshot`, `libelle_snapshot` dans `ligne_commande`,
|
||||||
`label_snapshot` sur `order_item`, voir `dictionary.md` 3.11). Le total affiche
|
voir `dictionary.md` 3.6). Le total affiche par le client n'est pas considere
|
||||||
par le client n'est pas considere comme la source de verite : ceci limite la
|
comme la source de verite : ceci limite la falsification du prix cote client.
|
||||||
falsification du prix cote client.
|
|
||||||
|
|
||||||
### 4.2 Creation atomique (create + pay)
|
### 4.2 Transitions de statut
|
||||||
|
|
||||||
Le parcours materialise les transitions T1 et T2 de
|
Le parcours materialise les transitions T1 et T2 de
|
||||||
`docs/uml/state-commande.md` dans **un seul appel et une seule transaction** :
|
`docs/uml/state-commande.md`, en deux phases successives conformes a la regle
|
||||||
|
metier :
|
||||||
|
|
||||||
- `POST /api/orders` cree la commande en `pending_payment` (T1) puis la fait
|
- `POST /api/orders` cree la commande composee en `pending_payment` (T1).
|
||||||
passer a `paid` (T2) avant le `COMMIT`. `paid_at` est renseigne.
|
- `POST /api/orders/{id}/pay` enregistre le paiement et fait passer la commande
|
||||||
- La saisie du numero de retrait tient lieu de paiement (cadre RNCP) ; il n'y a
|
a `paid` (T2), avec l'horodatage `paye_a`.
|
||||||
pas d'appel `POST /api/orders/{id}/pay` separe (supprime par rapport au v0.1).
|
|
||||||
- Le decrement du stock (`ingredient.stock_quantity`) et la journalisation
|
|
||||||
(`stock_movement` type `sale`) sont inclus dans la meme transaction que
|
|
||||||
l'insert de la commande : soit tout reussit, soit tout est annule (`ROLLBACK`).
|
|
||||||
|
|
||||||
Le statut `pending_payment` n'est donc pas observable en dehors de la
|
La separation des deux appels reflete les deux phases du cycle de vie :
|
||||||
transaction (coherent avec `mct.md` section 13).
|
composer la commande, puis la payer.
|
||||||
|
|
||||||
### 4.3 Panier local jusqu'a la validation
|
### 4.3 Panier local jusqu'a la validation
|
||||||
|
|
||||||
|
|
@ -177,20 +166,9 @@ navigateur peut etre envisage plus tard.
|
||||||
|
|
||||||
`PROJECT_CONTEXT.md` section 4 prevoit un mode de repli ou la Borne lit des
|
`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
|
fichiers JSON statiques si l'API est indisponible. Ce mode concerne uniquement
|
||||||
les lectures (phases 1 a 2). La validation et la creation (phase 4) requierent
|
les lectures (phases 1 a 2). La validation (phase 4) et le paiement (phase 5)
|
||||||
l'API ; sans elle, la commande n'est ni persistee ni payee. Ce cas degrade
|
requierent l'API ; sans elle, la commande n'est ni persistee ni payee. Ce cas
|
||||||
n'est pas detaille dans le diagramme nominal ci-dessus.
|
degrade n'est pas detaille dans le diagramme nominal ci-dessus.
|
||||||
|
|
||||||
### 4.5 Garde-fous securite a venir (passe security-by-design)
|
|
||||||
|
|
||||||
Le flux ci-dessus est la cible **fonctionnelle** v0.2. La passe security-by-design
|
|
||||||
ajoutera, en complement (append, sans reecrire ce flux), des garde-fous sur le
|
|
||||||
`POST /api/orders` anonyme : cle d'idempotence (`idempotency_key` UNIQUE pour
|
|
||||||
dedupliquer les POST rejoues), limitation de debit / anti-spam, et verrou
|
|
||||||
pessimiste `SELECT ... FOR UPDATE` sur les ingredients pendant le decrement
|
|
||||||
(anti-oversell multi-bornes). Ces ajouts dependent de decisions encore a
|
|
||||||
trancher (oversell/idempotence, throttling) et seront documentes dans un artefact
|
|
||||||
`docs/uml/security-sequence.md` dedie.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -198,21 +176,18 @@ trancher (oversell/idempotence, throttling) et seront documentes dans un artefac
|
||||||
|
|
||||||
| Verification | Resultat |
|
| Verification | Resultat |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Endpoints utilises existent dans `PROJECT_CONTEXT.md` section 7 | `GET /api/categories`, `GET /api/products`, `GET /api/menus`, `POST /api/orders` ; l'appel `POST /api/orders/{id}/pay` du v0.1 est supprime (creation atomique) |
|
| 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 / dictionnaire | Oui : `category`, `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `stock_movement` |
|
| 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), atomiques |
|
| Statuts utilises coherents avec `state-commande.md` | Oui : `pending_payment` puis `paid` (T1, T2), valeurs ENUM anglaises |
|
||||||
| Operation MCT correspondante | `mct.md` 3.3 CREATE_ORDER (transaction unique, snapshots, decrement stock, transition atomique) |
|
| Format de reponse JSON | Coherent avec `PROJECT_CONTEXT.md` section 7 (`{data, error}`) et la reponse `{id, number, status}` du POST orders |
|
||||||
| Format de reponse JSON | Coherent avec `PROJECT_CONTEXT.md` section 7 (`{data, error}`) et la reponse `{id, order_number, status, total_ttc}` du POST orders |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Arbitrage tranche
|
## 6. Arbitrage tranche
|
||||||
|
|
||||||
La phase de paiement separee du v0.1 (`POST /api/orders/{id}/pay`) est supprimee :
|
La phase de paiement est integree au flux conformement a la regle metier des
|
||||||
la creation et le passage a `paid` sont atomiques dans `POST /api/orders`,
|
deux phases (composer puis payer). La sequence suit la machine canonique de
|
||||||
conformement au MCT v0.2 (3.3) et a la regle metier (saisie du numero = substitut
|
`state-commande.md` : creation en `pending_payment` (T1) puis paiement vers
|
||||||
de paiement). Le decrement de stock et la journalisation `stock_movement` sont
|
`paid` (T2), avec des valeurs ENUM en anglais. Point a confirmer au MCT :
|
||||||
inclus dans la meme transaction, garantissant la coherence stock/commande. Les
|
l'endpoint de paiement (`POST /api/orders/{id}/pay`) doit etre reporte dans la
|
||||||
valeurs ENUM sont en anglais (`pending_payment`, `paid`). Les garde-fous de
|
section 7 du brief s'il n'y figure pas encore.
|
||||||
securite (idempotence, rate-limit, verrou pessimiste) relevent de la passe
|
|
||||||
security-by-design et seront ajoutes en complement (section 4.5).
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Diagramme d'etats-transitions - Commande
|
# Diagramme d'etats-transitions - Commande
|
||||||
|
|
||||||
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
||||||
**Statut** : v0.2 - prod-like, machine a 4 etats
|
**Statut** : v0.1
|
||||||
**Date** : 2026-06-11
|
**Date** : 2026-05-21
|
||||||
**Branche** : `feat/p1-conception`
|
**Branche** : `feat/p1-conception`
|
||||||
**Auteur methodologie** : BYAN
|
**Auteur methodologie** : BYAN
|
||||||
|
|
||||||
|
|
@ -10,41 +10,38 @@
|
||||||
|
|
||||||
## 1. Objet du document
|
## 1. Objet du document
|
||||||
|
|
||||||
Ce document formalise la **machine a etats** de l'attribut `customer_order.status`.
|
Ce document formalise la **machine a etats** de l'attribut `commande.statut`.
|
||||||
Il decrit les etats possibles d'une commande, les transitions autorisees entre
|
Il decrit les etats possibles d'une commande, les transitions autorisees entre
|
||||||
ces etats, les **evenements** qui les declenchent et les **gardes** (conditions)
|
ces etats, les **evenements** qui les declenchent et les **gardes** (conditions)
|
||||||
qui les conditionnent.
|
qui les conditionnent.
|
||||||
|
|
||||||
Il complete le MCD (`docs/merise/mcd.md`, cycle de vie de la commande), le
|
Il complete le MCD (`docs/merise/mcd.md` section 9, qui esquisse le cycle de
|
||||||
dictionnaire (`docs/merise/dictionary.md` 3.10, qui declare l'ENUM `status`) et
|
vie) et le dictionnaire (`docs/merise/dictionary.md` 3.5, qui declare l'ENUM).
|
||||||
le MCT (`docs/merise/mct.md` section 13, qui resume les transitions par
|
|
||||||
operation).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Source de verite et regle metier
|
## 2. Source de verite et regle metier
|
||||||
|
|
||||||
Le modele v0.2 (prod-like) reduit la machine a **quatre etats**. La regle metier
|
La regle metier confirmee fixe deux phases successives dans le cycle de vie
|
||||||
distingue la **composition payee** de la **remise** : une commande est creee et
|
d'une commande : le client **compose** sa commande, **puis** il **paie**. Une
|
||||||
payee en une operation atomique (la saisie du numero de retrait tient lieu de
|
fois payee, la commande entre en preparation. Le paiement fait partie integrante
|
||||||
paiement dans le cadre RNCP), puis elle est remise au client en un geste unique.
|
du cycle. Les valeurs d'etat sont en anglais et alignees sur l'ENUM du
|
||||||
|
dictionnaire.
|
||||||
|
|
||||||
| Source | Valeurs de statut |
|
| Source | Valeurs de statut |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `dictionary.md` 3.10 (ENUM SQL) | `pending_payment`, `paid`, `delivered`, `cancelled` |
|
| `dictionary.md` 3.5 (ENUM SQL) | `pending_payment`, `paid`, `preparing`, `ready`, `delivered`, `cancelled` |
|
||||||
| `mct.md` section 13 (transitions) | creer+payer -> remettre, annulation depuis tout etat non terminal |
|
| Regle metier confirmee | composer -> payer -> preparer -> pret -> remettre |
|
||||||
|
|
||||||
> Le dictionnaire (`dictionary.md` 3.10) et la machine ci-dessous partagent la
|
**Machine a etats canonique** : la machine ci-dessous est la seule autorisee.
|
||||||
> meme ENUM a 4 valeurs, ce qui maintient la coherence entre le modele de
|
Elle suit l'ENUM du dictionnaire et la regle metier des deux phases :
|
||||||
> donnees et le modele d'etats (cross-validation, mantra #34).
|
|
||||||
|
|
||||||
**Etats supprimes par rapport au v0.1** : `preparing` et `ready`. En contexte
|
- `pending_payment` : commande composee, en attente de paiement.
|
||||||
fast-food, l'affichage cuisine (KDS) est un dispositif visuel : l'equipier lit
|
- `paid` : paiement effectue ; la commande peut entrer en file de preparation.
|
||||||
le ticket et agit. Ces deux etats intermediaires ajoutaient des transitions sans
|
|
||||||
valeur metier proportionnelle. La cuisine est en **lecture seule** ; la remise
|
> Le dictionnaire (`dictionary.md` 3.5) et la machine ci-dessous partagent la
|
||||||
(`DELIVER_ORDER`) est le geste unique qui fait avancer le statut. Le KPI est le
|
> meme ENUM, ce qui maintient la coherence entre le modele de donnees et le
|
||||||
temps total `delivered_at - paid_at` (SLA ~10 min) ; la couleur du KDS est
|
> modele d'etats (cross-validation, mantra #34).
|
||||||
calculee a l'affichage depuis `now - paid_at`, sans etat stocke supplementaire.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -52,16 +49,12 @@ calculee a l'affichage depuis `now - paid_at`, sans etat stocke supplementaire.
|
||||||
|
|
||||||
| Etat | Valeur ENUM | Signification | Acteur qui declenche l'entree |
|
| Etat | Valeur ENUM | Signification | Acteur qui declenche l'entree |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| En attente de paiement | `pending_payment` | Etat initial transitoire : commande composee, en attente de paiement. Non observable hors transaction (voir section 7). | Client (kiosk) ou Counter/Drive (back-office) |
|
| 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 entre en file de preparation (lecture seule cuisine). | Client (kiosk) ou Counter/Drive |
|
| Payee | `paid` | Paiement effectue ; la commande peut entrer en file de preparation. | Client (paiement) ou Accueil |
|
||||||
| Livree | `delivered` | Remise effectuee au client. Etat **final**. | Counter ou Drive |
|
| En preparation | `preparing` | Prise en charge par la Preparation, en cuisine. | Preparation |
|
||||||
| Annulee | `cancelled` | Commande abandonnee ou annulee avant remise. Etat **final**. | Counter, Drive ou Admin |
|
| Prete | `ready` | Preparation terminee, prete au comptoir. | Preparation |
|
||||||
|
| Livree | `delivered` | Remise effectuee au client. Etat **final**. | Accueil |
|
||||||
`pending_payment` est l'etat par defaut a l'INSERT (`dictionary.md` 3.10), mais
|
| Annulee | `cancelled` | Commande abandonnee ou annulee. Etat **final**. | Client, Accueil ou Administration |
|
||||||
la transition vers `paid` est realisee dans la **meme transaction** que la
|
|
||||||
creation (operations `CREATE_ORDER` / `CREATE_COUNTER_ORDER` du MCT). Il est donc
|
|
||||||
conserve dans l'ENUM pour la lisibilite du cycle et pour laisser la porte
|
|
||||||
ouverte a un paiement reel ulterieur sans migration destructive.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -69,13 +62,19 @@ ouverte a un paiement reel ulterieur sans migration destructive.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
stateDiagram-v2
|
stateDiagram-v2
|
||||||
[*] --> pending_payment : creer la commande (kiosk / counter / drive)
|
[*] --> pending_payment : creer commande (kiosk / counter / drive)
|
||||||
|
|
||||||
pending_payment --> paid : payer\n[atomique dans CREATE_ORDER / CREATE_COUNTER_ORDER\nsaisie du numero = substitut de paiement]
|
pending_payment --> paid : payer\n[panier contient au moins 1 ligne]
|
||||||
pending_payment --> cancelled : annuler\n[avant remise]
|
pending_payment --> cancelled : abandonner\n[avant paiement]
|
||||||
|
|
||||||
paid --> delivered : remettre au client\n[acteur Counter / Drive, geste unique]
|
paid --> preparing : prendre en charge\n[acteur Preparation, file triee par heure croissante]
|
||||||
paid --> cancelled : annuler\n[Counter / Drive / Admin, re-credit du stock]
|
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 --> [*]
|
delivered --> [*]
|
||||||
cancelled --> [*]
|
cancelled --> [*]
|
||||||
|
|
@ -87,29 +86,29 @@ stateDiagram-v2
|
||||||
|
|
||||||
| # | De | Vers | Evenement declencheur | Garde (condition) | Acteur |
|
| # | De | Vers | Evenement declencheur | Garde (condition) | Acteur |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| T1 | (initial) | `pending_payment` | Creation de la commande composee | Au moins une ligne (`order_item`) ; numero de retrait non vide | Client / Counter / Drive |
|
| 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 (atomique a la creation) | La commande contient au moins une `order_item` ; decrement du stock et insert dans la meme transaction | Client / Counter / Drive |
|
| 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 | Counter / Drive / Admin |
|
| T3 | `pending_payment` | `cancelled` | Abandon avant paiement | Commande pas encore payee | Client / Accueil |
|
||||||
| T4 | `paid` | `delivered` | Remise physique au client | La commande est `paid` ; l'acteur detient `order.deliver` ; source compatible avec son role (`role_visible_source`) | Counter / Drive |
|
| 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 remise | L'acteur detient `order.cancel` ; le stock consomme est re-credite (`stock_movement` type `cancellation`) | Counter / Drive / Admin |
|
| 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
|
### Invariants de la machine a etats
|
||||||
|
|
||||||
- `delivered` et `cancelled` sont des etats **finaux** : aucune transition n'en
|
- `delivered` et `cancelled` sont des etats **finaux** : aucune transition n'en
|
||||||
sort.
|
sort.
|
||||||
- Aucune transition ne revient en arriere. Une erreur operationnelle se traite
|
- Aucune transition ne revient en arriere (pas de `preparing -> paid`). Une
|
||||||
par annulation puis nouvelle commande, pour preserver l'integrite de
|
erreur operationnelle se traite par annulation puis nouvelle commande, pour
|
||||||
l'historique et des snapshots (`label_snapshot`, `unit_price_cents_snapshot`,
|
preserver l'integrite de l'historique et des snapshots de prix.
|
||||||
`vat_rate_snapshot` sur `order_item`).
|
- La transition vers `cancelled` est possible depuis tous les etats **sauf**
|
||||||
- La transition vers `cancelled` est possible depuis `pending_payment` et
|
`delivered` (une commande remise ne s'annule pas dans ce modele). Ceci est
|
||||||
`paid`, mais pas depuis `delivered` (une commande remise ne s'annule pas dans
|
coherent avec `mcd.md` section 9 : "Annuler : transition vers `cancelled`
|
||||||
ce modele). Coherent avec `mct.md` 7.1 (`CANCEL_ORDER`).
|
(depuis tout statut sauf `delivered`)".
|
||||||
- `paid_at` (DATETIME, `dictionary.md` 3.10) est renseigne a la transition T2.
|
- `paye_a` (DATETIME, `dictionary.md` 3.5) est renseigne au moment de la
|
||||||
`delivered_at` est renseigne a T4. `cancelled_at` est renseigne a T3/T5. Les
|
transition T2 (`pending_payment -> paid`) et reste NULL avant.
|
||||||
trois colonnes sont NULL tant que la transition correspondante n'a pas eu lieu.
|
|
||||||
- La cuisine (`kitchen`) ne declenche aucune transition : son acces a la file
|
|
||||||
des commandes `paid` est en **lecture seule** (`mct.md` 5.1,
|
|
||||||
`LIST_ORDERS_DISPLAY`).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -117,36 +116,29 @@ stateDiagram-v2
|
||||||
|
|
||||||
| Verification | Resultat |
|
| Verification | Resultat |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Tous les etats du diagramme existent dans l'ENUM `dictionary.md` 3.10 | Oui (4 valeurs, toutes utilisees) |
|
| 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 `mct.md` 7.1 | Respectee (T3, T5 ; pas de transition depuis `delivered`) |
|
| La regle "annulation possible sauf depuis delivered" de `mcd.md` 9 | Respectee (T5, T7, T9 ; pas de transition depuis `delivered`) |
|
||||||
| Transition `paid -> delivered` en geste unique de `mct.md` 6.1 | Couvert par T4 (`DELIVER_ORDER`) |
|
| Cycle de vie esquisse dans `mcd.md` 9 | Couvert : `pending_payment` -> `paid` (payer), `paid` -> `preparing` (preparer), `preparing` -> `ready` (marquer pret), `ready` -> `delivered` (remettre) |
|
||||||
| Atomicite `pending_payment -> paid` de `mct.md` 3.3 / 4.1 / section 13 | Couvert par T2 (saisie du numero = substitut de paiement) |
|
| Acteurs de `use-cases.md` | Preparation declenche T4/T6/T7 ; Accueil declenche T8/T9 ; Administration peut annuler |
|
||||||
| Acteurs de `use-cases.md` | Counter/Drive declenchent T4/T5 ; Admin peut annuler (T3/T5) ; Kitchen en lecture seule |
|
|
||||||
| Timestamps de phase `paid_at` / `delivered_at` / `cancelled_at` | Renseignes a T2 / T4 / T3-T5 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Arbitrage tranche
|
## 7. Arbitrage tranche
|
||||||
|
|
||||||
La machine retenue est reduite a quatre etats (Decision 4,
|
La divergence historique entre l'ENUM du dictionnaire et un parcours sans
|
||||||
`docs/notes/revue-alignement-p1.md` section 7) :
|
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 -> delivered
|
pending_payment -> paid -> preparing -> ready -> delivered
|
||||||
| |
|
(cancelled atteignable depuis pending_payment, paid, preparing)
|
||||||
+------------+--------> cancelled (depuis pending_payment ou paid)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note sur la transition `pending_payment -> paid`** : dans le cadre RNCP, le
|
Cette machine est la source de verite partagee par `dictionary.md` 3.5,
|
||||||
paiement est remplace par la saisie du numero de commande (kiosk) ou par la
|
`use-cases.md` (cas "Payer la commande" cote Client) et
|
||||||
validation de l'equipier (counter/drive). La transition est **atomique** dans
|
`sequence-passer-commande.md` (etape paiement entre validation du panier et
|
||||||
`CREATE_ORDER` et `CREATE_COUNTER_ORDER` : le statut `pending_payment` n'est pas
|
confirmation). La colonne `paye_a` est renseignee a la transition T2. A
|
||||||
observable en dehors de la transaction de creation. Il reste declare dans l'ENUM
|
revalider lors du MCT.
|
||||||
pour exprimer le cycle de vie complet et pour autoriser un paiement reel
|
|
||||||
ulterieur (cout d'une valeur d'ENUM).
|
|
||||||
|
|
||||||
Les etats `preparing` et `ready` du v0.1 sont supprimes : la cuisine est un
|
|
||||||
affichage visuel en lecture seule, et la remise fusionne preparation+remise en
|
|
||||||
un geste unique (`DELIVER_ORDER`). Effet de bord propage : les operations
|
|
||||||
`MARK_IN_PREPARATION` et `MARK_READY` disparaissent du MCT (voir `mct.md`
|
|
||||||
sections 1 et 13).
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Diagramme de cas d'utilisation - Wakdo
|
# Diagramme de cas d'utilisation - Wakdo
|
||||||
|
|
||||||
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
**Phase UML** : P1 - Conception, complement UML (apres MCD)
|
||||||
**Statut** : v0.2 - prod-like, 5 roles RBAC + catalogue de 23 permissions
|
**Statut** : v0.1
|
||||||
**Date** : 2026-06-11
|
**Date** : 2026-05-21
|
||||||
**Branche** : `feat/p1-conception`
|
**Branche** : `feat/p1-conception`
|
||||||
**Auteur methodologie** : BYAN
|
**Auteur methodologie** : BYAN
|
||||||
|
|
||||||
|
|
@ -12,247 +12,174 @@
|
||||||
|
|
||||||
Ce document recense les **cas d'utilisation** de Wakdo, c'est-a-dire les
|
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
|
fonctionnalites observables du systeme du point de vue de ses acteurs. Il
|
||||||
complete le MCD (`docs/merise/mcd.md`), le dictionnaire
|
complete le MCD (`docs/merise/mcd.md`) et le dictionnaire
|
||||||
(`docs/merise/dictionary.md`) et le MCT (`docs/merise/mct.md`, 26 operations) en
|
(`docs/merise/dictionary.md`) en passant de la vue **donnees** a la vue
|
||||||
passant de la vue **donnees / traitements** a la vue **usages**.
|
**usages**.
|
||||||
|
|
||||||
Le diagramme reste au niveau conceptuel : il identifie qui fait quoi, sans
|
Le diagramme reste au niveau conceptuel. Il ne prejuge pas de l'ecran ou de
|
||||||
prejuger de l'ecran ou de l'endpoint qui realise chaque cas. Chaque cas
|
l'endpoint qui realisera chaque cas, mais identifie qui fait quoi.
|
||||||
back-office est rattache a la **permission** qui le conditionne (catalogue fige
|
|
||||||
de 23 codes, `dictionary.md` 3.17), conformement a la regle RBAC
|
|
||||||
permission-driven : le code teste une permission, pas un nom de role.
|
|
||||||
|
|
||||||
**Sources** :
|
**Sources** :
|
||||||
- `docs/PROJECT_CONTEXT.md` sections 2 (acteurs, processus), 7 (scope back-office)
|
- `docs/PROJECT_CONTEXT.md` sections 2 (acteurs, processus), 7 (scope RBAC)
|
||||||
- `docs/merise/dictionary.md` 3.14-3.18 (`user`, `role`, `role_visible_source`, `permission`, `role_permission`)
|
- `docs/merise/dictionary.md` (entites `commande`, `role`, `user`)
|
||||||
- `docs/merise/mct.md` (operations, acteurs, permissions par operation)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Acteurs - perimetre et challenge de pertinence
|
## 2. Acteurs - perimetre et challenge de pertinence
|
||||||
|
|
||||||
Le brief initial (`PROJECT_CONTEXT.md` section 2) decrivait quatre acteurs
|
Le brief (`PROJECT_CONTEXT.md` section 2 et section 7) definit les acteurs
|
||||||
metier (Client, Accueil, Preparation, Administration) adosses a 3 roles RBAC. Le
|
metier. Avant de les retenir, chaque acteur propose dans la consigne initiale
|
||||||
modele v0.2 (prod-like, Decision 4 de `revue-alignement-p1.md` section 7) raffine
|
est confronte au perimetre reel du projet.
|
||||||
le back-office en **5 roles** pour coller a l'organisation reelle d'un fast-food
|
|
||||||
multi-canal. Chaque acteur candidat est confronte au perimetre reel.
|
|
||||||
|
|
||||||
| Acteur candidat (brief) | Statut v0.2 | Justification (perimetre reel) |
|
| Acteur candidat | Statut | Justification (perimetre reel) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Client (borne kiosk)** | Retenu (acteur `CUSTOMER`) | Acteur central du Bloc 1. Compose et valide une commande sur la borne tactile autonome (canal `kiosk`). **Non authentifie**. |
|
| **Client (borne kiosk)** | Retenu | Acteur central du Bloc 1. Compose et valide une commande sur la borne tactile autonome (canal `kiosk`). Non authentifie. |
|
||||||
| **Accueil** | **Scinde** en `counter` et `drive` | Le besoin "Accueil" recouvre deux canaux operationnels distincts : le comptoir (`counter`) et le drive (`drive`). Le v0.2 les separe car le tag `source` de la commande et le filtre de dashboard (`role_visible_source`) different. Tous deux saisissent des commandes, les remettent et les annulent. |
|
| **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. |
|
||||||
| **Preparation** | Retenu, renomme `kitchen` | Role RBAC `kitchen`. Voit la file des commandes `paid` triees par `paid_at` croissant. **Lecture seule** : ne declenche aucune transition de statut (le KDS est un dispositif visuel ; la remise revient a `counter`/`drive`). |
|
| **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`. |
|
||||||
| **Administration** | **Scinde** en `admin` et `manager` | Le v0.1 fusionnait "Manager/Admin". Le v0.2 distingue : `admin` (gestion des utilisateurs, des roles et permissions, suppressions catalogue) et `manager` (catalogue create/update, stock/reappro, stats), sans acces aux utilisateurs ni au RBAC. Resout le point ouvert v0.1 "Manager vs Admin". |
|
| **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. |
|
||||||
| **Caisse** | Ecarte (recouvert par `counter`/`drive`) | Aucun role `caisse` n'existe. L'encaissement est atomique a la creation de commande (saisie du numero = substitut de paiement) ; il est realise par le Client (kiosk) ou par `counter`/`drive` (back-office). Resout le point ouvert v0.1 "Caisse absente du RBAC". |
|
| **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". |
|
||||||
| **Systeme** | Retenu (acteur `SYS`) | Logique interne (generation du numero, reponse API de confirmation). Apparait dans le MCT (3.4 `DISPLAY_CONFIRMATION`) ; non represente comme acteur humain au diagramme. |
|
|
||||||
|
|
||||||
### Decision sur les acteurs retenus
|
### Decision sur les acteurs retenus
|
||||||
|
|
||||||
Six acteurs sont conserves : un acteur public et cinq roles back-office.
|
Quatre acteurs sont conserves au diagramme :
|
||||||
|
|
||||||
1. **Customer** (borne kiosk, non authentifie)
|
1. **Client** (borne, non authentifie)
|
||||||
2. **Admin** (role `admin`)
|
2. **Administration** (role `admin`)
|
||||||
3. **Manager** (role `manager`)
|
3. **Preparation** (role `preparation`, ex-"Cuisine")
|
||||||
4. **Kitchen** (role `kitchen`, ex-"Preparation", lecture seule)
|
4. **Accueil** (role `accueil`, recouvre le besoin "Caisse")
|
||||||
5. **Counter** (role `counter`, ex-"Accueil" comptoir)
|
|
||||||
6. **Drive** (role `drive`, ex-"Accueil" drive)
|
|
||||||
|
|
||||||
> Regle RBAC permission-driven (`dictionary.md` 3.15) : les rattachements
|
> Decision actee : il n'y a **pas** de parcours employe dedie modelise a part.
|
||||||
> acteur -> cas ci-dessous refletent la **matrice de permissions par defaut au
|
> Les cas d'usage des employes (Administration, Preparation, Accueil) sont
|
||||||
> seed**. Le gardien reel est la permission, pas le nom du role : un role
|
> couverts directement ici. Cette decision suit le mantra du Rasoir d'Ockham
|
||||||
> personnalise (ex. "chef-patissier") dote des bonnes permissions ouvre les
|
> (#37) : on evite une couche de modelisation redondante tant qu'aucun besoin
|
||||||
> memes cas, sans changement de code. Les 5 roles seed sont un point de depart,
|
> ne la justifie.
|
||||||
> pas une liste fermee.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Diagramme de cas d'utilisation
|
## 3. Diagramme de cas d'utilisation
|
||||||
|
|
||||||
Mermaid ne fournit pas de type `usecase` natif. La representation utilise un
|
Mermaid ne fournit pas de type `usecase` natif. La representation ci-dessous
|
||||||
`flowchart` : les acteurs sont a gauche, les cas d'utilisation regroupes par
|
utilise un `flowchart` : les acteurs sont des noeuds a gauche, les cas
|
||||||
sous-systeme. La permission qui conditionne chaque cas back-office est precisee
|
d'utilisation sont des noeuds arrondis regroupes par sous-systeme, et les
|
||||||
en section 4.
|
fleches portent les relations (`<<include>>`, `<<extend>>`) la ou elles
|
||||||
|
ont du sens.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
%% Acteurs
|
%% Acteurs
|
||||||
Customer(("Customer<br/>borne kiosk<br/>non authentifie"))
|
Client(("Client<br/>borne kiosk"))
|
||||||
Admin(("Admin<br/>role admin"))
|
Admin(("Administration<br/>role admin"))
|
||||||
Manager(("Manager<br/>role manager"))
|
Prep(("Preparation<br/>role preparation"))
|
||||||
Kitchen(("Kitchen<br/>role kitchen<br/>lecture seule"))
|
Accueil(("Accueil<br/>role accueil"))
|
||||||
Counter(("Counter<br/>role counter"))
|
|
||||||
Drive(("Drive<br/>role drive"))
|
|
||||||
|
|
||||||
%% Sous-systeme Borne client
|
%% Sous-systeme Borne client
|
||||||
subgraph BORNE["Borne client - Bloc 1 (public)"]
|
subgraph BORNE["Borne client - Bloc 1"]
|
||||||
UC1(["Consulter le catalogue"])
|
UC1(["Consulter le catalogue"])
|
||||||
UC2(["Composer le panier"])
|
UC2(["Composer un menu"])
|
||||||
UC3(["Consulter les allergenes"])
|
UC3(["Passer une commande"])
|
||||||
UC4(["Passer une commande"])
|
UC4(["Saisir le numero de retrait"])
|
||||||
UC5(["Saisir le numero de retrait"])
|
UC5(["Recevoir la confirmation"])
|
||||||
UC6(["Recevoir la confirmation"])
|
UC6(["Payer la commande"])
|
||||||
end
|
end
|
||||||
|
|
||||||
%% Sous-systeme Operations commande
|
%% Sous-systeme Back-office
|
||||||
subgraph OPS["Operations commande - back-office"]
|
subgraph BACK["Back-office - Bloc 2"]
|
||||||
UC10(["Saisir une commande<br/>comptoir / drive"])
|
UC10(["Gerer le catalogue<br/>categories, produits, menus"])
|
||||||
UC11(["Consulter la file de preparation"])
|
UC11(["Gerer les utilisateurs et roles"])
|
||||||
UC12(["Remettre la commande"])
|
UC12(["Consulter les statistiques"])
|
||||||
UC13(["Annuler une commande"])
|
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
|
end
|
||||||
|
|
||||||
%% Sous-systeme Catalogue
|
%% Relations Client
|
||||||
subgraph CAT["Catalogue - back-office"]
|
Client --> UC1
|
||||||
UC20(["Gerer produits"])
|
Client --> UC2
|
||||||
UC21(["Gerer menus et slots"])
|
Client --> UC3
|
||||||
UC22(["Gerer categories"])
|
Client --> UC6
|
||||||
UC23(["Gerer ingredients,<br/>compositions, allergenes"])
|
Client --> UC5
|
||||||
end
|
|
||||||
|
|
||||||
%% Sous-systeme Stock
|
%% include / extend cote borne
|
||||||
subgraph STK["Stock - back-office"]
|
UC3 -. include .-> UC4
|
||||||
UC30(["Consulter le stock"])
|
UC3 -. include .-> UC6
|
||||||
UC31(["Compter l'inventaire"])
|
|
||||||
UC32(["Reapprovisionner"])
|
|
||||||
end
|
|
||||||
|
|
||||||
%% Sous-systeme Administration
|
|
||||||
subgraph ADM["Administration - back-office"]
|
|
||||||
UC40(["Gerer les utilisateurs"])
|
|
||||||
UC41(["Gerer roles et permissions"])
|
|
||||||
UC42(["Consulter les statistiques"])
|
|
||||||
end
|
|
||||||
|
|
||||||
%% Transverse
|
|
||||||
UC50(["S'authentifier"])
|
|
||||||
UC51(["Se deconnecter"])
|
|
||||||
|
|
||||||
%% Relations Customer
|
|
||||||
Customer --> UC1
|
|
||||||
Customer --> UC2
|
|
||||||
Customer --> UC4
|
|
||||||
Customer --> UC6
|
|
||||||
UC2 -. include .-> UC1
|
UC2 -. include .-> UC1
|
||||||
UC2 -. extend .-> UC3
|
UC3 -. extend .-> UC2
|
||||||
UC4 -. include .-> UC5
|
|
||||||
UC4 -. include .-> UC2
|
|
||||||
|
|
||||||
%% Relations Counter / Drive (operations commande + stock)
|
%% Relations Administration
|
||||||
Counter --> UC10
|
|
||||||
Counter --> UC11
|
|
||||||
Counter --> UC12
|
|
||||||
Counter --> UC13
|
|
||||||
Drive --> UC10
|
|
||||||
Drive --> UC11
|
|
||||||
Drive --> UC12
|
|
||||||
Drive --> UC13
|
|
||||||
UC10 -. include .-> UC1
|
|
||||||
|
|
||||||
%% Kitchen (lecture seule)
|
|
||||||
Kitchen --> UC11
|
|
||||||
|
|
||||||
%% Stock (kitchen / counter / drive / manager / admin)
|
|
||||||
Kitchen --> UC30
|
|
||||||
Kitchen --> UC31
|
|
||||||
Counter --> UC30
|
|
||||||
Counter --> UC31
|
|
||||||
Drive --> UC30
|
|
||||||
Drive --> UC31
|
|
||||||
Manager --> UC30
|
|
||||||
Manager --> UC31
|
|
||||||
Manager --> UC32
|
|
||||||
|
|
||||||
%% Catalogue (manager + admin)
|
|
||||||
Manager --> UC20
|
|
||||||
Manager --> UC21
|
|
||||||
Manager --> UC22
|
|
||||||
Manager --> UC23
|
|
||||||
Admin --> UC20
|
|
||||||
Admin --> UC21
|
|
||||||
Admin --> UC22
|
|
||||||
Admin --> UC23
|
|
||||||
|
|
||||||
%% Administration (admin) + stats (manager + admin)
|
|
||||||
Admin --> UC40
|
Admin --> UC40
|
||||||
Admin --> UC41
|
Admin --> UC10
|
||||||
Admin --> UC42
|
|
||||||
Admin --> UC30
|
|
||||||
Admin --> UC31
|
|
||||||
Admin --> UC32
|
|
||||||
Admin --> UC11
|
Admin --> UC11
|
||||||
Admin --> UC13
|
Admin --> UC12
|
||||||
Manager --> UC42
|
|
||||||
|
|
||||||
%% Authentification mutualisee (tout cas back-office)
|
%% Relations Preparation
|
||||||
UC10 -. include .-> UC50
|
Prep --> UC40
|
||||||
UC11 -. include .-> UC50
|
Prep --> UC20
|
||||||
UC20 -. include .-> UC50
|
Prep --> UC21
|
||||||
UC30 -. include .-> UC50
|
|
||||||
UC40 -. include .-> UC50
|
%% Relations Accueil
|
||||||
UC42 -. include .-> UC50
|
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. Description des cas d'utilisation
|
||||||
|
|
||||||
### 4.1 Acteur Customer (borne kiosk, non authentifie)
|
### 4.1 Acteur Client (borne kiosk)
|
||||||
|
|
||||||
| Cas | Operation MCT | Description | Entites manipulees |
|
| Cas | Description | Entites manipulees |
|
||||||
|---|---|---|---|
|
|
||||||
| Consulter le catalogue | 3.1 LOAD_CATALOGUE | Parcourir categories, produits et menus disponibles, charges via `GET /api/categories`, `/api/products`, `/api/menus` (ou JSON fallback). | `category`, `product`, `menu`, `menu_slot`, `menu_slot_option` |
|
|
||||||
| Composer le panier | 3.2 COMPOSE_CART | Ajouter produits a la carte ou menus ; remplir les slots d'un menu (`order_item_selection`), choisir le format Normal/Maxi, ajouter/retirer des ingredients (`order_item_modifier`). Panier volatil cote front, aucun ecrit BDD a ce stade. | `product`, `menu`, `menu_slot`, `menu_slot_option`, `ingredient`, `product_ingredient` |
|
|
||||||
| Consulter les allergenes | (derive de 3.1) | Afficher en modal les allergenes d'un produit, **calcules** par jointure `product_ingredient -> ingredient_allergen -> allergen` (INCO 1169/2011). Etend la composition. | `allergen`, `ingredient_allergen`, `product_ingredient` |
|
|
||||||
| Passer une commande | 3.3 CREATE_ORDER | Valider le panier et saisir le numero de retrait. Creation atomique : INSERT `customer_order` (`pending_payment` puis `paid` dans la meme transaction), `order_item` + selections + modifiers snapshotes, decrement du stock + `stock_movement`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient`, `stock_movement` |
|
|
||||||
| Saisir le numero de retrait | (inclus dans 3.3) | Renseigner le numero qui identifie le client. Tient lieu de paiement (cadre RNCP). Cas inclus par "Passer une commande". | `customer_order.order_number` |
|
|
||||||
| Recevoir la confirmation | 3.4 DISPLAY_CONFIRMATION | Afficher l'ecran de confirmation avec le numero, apres reponse `201` (statut `paid`). La borne se reinitialise. | `customer_order` |
|
|
||||||
|
|
||||||
### 4.2 Acteurs Counter et Drive (roles `counter`, `drive`)
|
|
||||||
|
|
||||||
| Cas | Operation MCT | Permission | Description | Entites |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Saisir une commande comptoir/drive | 4.1 CREATE_COUNTER_ORDER | `order.create` | Composer une commande pour un client au comptoir (`counter`) ou au drive (`drive`). Logique identique a CREATE_ORDER ; `source` auto-tague depuis `role.order_source`. Numero `C-`/`D-YYYY-MM-DD-NNN`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `ingredient`, `stock_movement` |
|
|
||||||
| Consulter la file de preparation | 5.1 LIST_ORDERS_DISPLAY | `order.read` | Voir les commandes `paid` triees par `paid_at` croissant, filtrees par `role_visible_source` (counter voit kiosk+counter ; drive voit drive). Couleur KDS = `now - paid_at`. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` |
|
|
||||||
| Remettre la commande | 6.1 DELIVER_ORDER | `order.deliver` | Geste unique `paid -> delivered`, `delivered_at = NOW()`. | `customer_order` |
|
|
||||||
| Annuler une commande | 7.1 CANCEL_ORDER | `order.cancel` | Transition vers `cancelled` depuis `pending_payment`/`paid`, `cancelled_at = NOW()`. Re-credit du stock si `paid`. | `customer_order`, `ingredient`, `stock_movement` |
|
|
||||||
|
|
||||||
### 4.3 Acteur Kitchen (role `kitchen`, lecture seule)
|
|
||||||
|
|
||||||
| Cas | Operation MCT | Permission | Description | Entites |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Consulter la file de preparation | 5.1 LIST_ORDERS_DISPLAY | `order.read` | Voir toutes les sources (kiosk, counter, drive) en lecture seule. **Aucune transition de statut** : le KDS est un affichage visuel. | `customer_order`, `order_item`, `order_item_selection`, `order_item_modifier`, `role_visible_source` |
|
|
||||||
|
|
||||||
### 4.4 Stock (Kitchen, Counter, Drive, Manager, Admin)
|
|
||||||
|
|
||||||
| Cas | Operation MCT | Permission | Description | Entites |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Consulter le stock | 9.3 READ_STOCK | `stock.read` | Lister les ingredients avec stock courant ; alerte rupture calculee a l'affichage (`stock_quantity <= low_stock_threshold`). | `ingredient`, `stock_movement` |
|
|
||||||
| Compter l'inventaire | 9.2 INVENTORY_COUNT | `stock.count` | Saisir un comptage physique ; le systeme enregistre l'ecart (`inventory_correction`). Inclut les equipiers (kitchen/counter/drive). | `ingredient`, `stock_movement` |
|
|
||||||
| Reapprovisionner | 9.1 RESTOCK | `stock.manage` | Enregistrer une livraison en conditionnements (`+= N * pack_size`). Reserve manager/admin. | `ingredient`, `stock_movement` |
|
|
||||||
|
|
||||||
### 4.5 Catalogue (Manager, Admin)
|
|
||||||
|
|
||||||
| Cas | Operation MCT | Permissions | Description | Entites |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Gerer produits | 8.1-8.3 CREATE/UPDATE/DELETE_PRODUCT | `product.create`/`update` (manager+admin), `product.delete` (admin seul) | CRUD produits (nom, prix, `vat_rate`, image, dispo). La suppression physique est reservee a `admin` et bloquee si reference (FK RESTRICT). | `product`, `category` |
|
|
||||||
| Gerer menus et slots | 8.4-8.6 CREATE/UPDATE/DELETE_MENU | `menu.create`/`update` (manager+admin), `menu.delete` (admin seul) | CRUD menus avec leur configuration de slots (`menu_slot`, `menu_slot_option`) et le burger fixe. | `menu`, `menu_slot`, `menu_slot_option`, `product` |
|
|
||||||
| Gerer categories | 8.7 MANAGE_CATEGORY | `category.manage` (manager+admin) | CRUD categories ; desactivation `is_active=0`. | `category` |
|
|
||||||
| Gerer ingredients, compositions, allergenes | 8.8 MANAGE_INGREDIENT | `ingredient.manage` (manager+admin) | CRUD `ingredient` ; composition `product_ingredient` (quantites Normal/Maxi, retirable/ajoutable, supplement) ; mapping `ingredient_allergen` (14 allergenes UE). | `ingredient`, `product_ingredient`, `ingredient_allergen`, `allergen` |
|
|
||||||
|
|
||||||
### 4.6 Administration (role `admin`) + Stats (Manager, Admin)
|
|
||||||
|
|
||||||
| Cas | Operation MCT | Permissions | Description | Entites |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Gerer les utilisateurs | 10.1-10.3 CREATE/UPDATE/DEACTIVATE_USER | `user.create`/`update`/`deactivate` (admin) ; `user.read` (admin+manager) | CRUD comptes back-office avec hash argon2id ; desactivation sans suppression (historique preserve). | `user`, `role` |
|
|
||||||
| Gerer roles et permissions | 10.4 MANAGE_RBAC | `role.manage` (admin) | Editer la matrice `role_permission`, creer/modifier des roles personnalises (`default_route`, `order_source`), regler `role_visible_source`. Permissions statiques (declarees en migration). | `role`, `permission`, `role_permission`, `role_visible_source` |
|
|
||||||
| Consulter les statistiques | 11.1 READ_STATS | `stats.read` (admin+manager) | Agregats par `service_day` (coupure 10h), top produits, taux d'annulation, temps moyen de remise `delivered_at - paid_at`, repartition par `source`/`service_mode`. | `customer_order`, `order_item` |
|
|
||||||
|
|
||||||
### 4.7 Cas transverses - Authentification
|
|
||||||
|
|
||||||
| Cas | Operation MCT | Description |
|
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| S'authentifier | 12.1 AUTHENTICATE_USER | Tous les roles back-office passent par ce cas avant d'acceder a leurs cas (relation `<<include>>`). Verification argon2id, regeneration de session (anti-fixation), `is_active=1` requis, redirection vers `role.default_route`. Le Customer du kiosk n'est pas authentifie. |
|
| 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` |
|
||||||
| Se deconnecter | 12.2 LOGOUT_USER | Destruction de session (`session_destroy()`) sur clic ou expiration (idle 4h / absolu 10h). |
|
| 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -261,32 +188,35 @@ flowchart LR
|
||||||
| Relation | Type | Justification |
|
| 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 -> Saisir le numero de retrait | include | La saisie du numero fait partie integrante de toute validation de commande. |
|
||||||
| Passer une commande -> Composer le panier | include | Une commande resulte d'un panier compose. |
|
| 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 le panier -> Consulter le catalogue | include | Composer suppose de parcourir les produits eligibles (a la carte ou par slot). |
|
| Composer un menu -> Consulter le catalogue | include | Composer un menu suppose de parcourir les produits eligibles a chaque slot. |
|
||||||
| Composer le panier -> Consulter les allergenes | extend | La consultation des allergenes est un cas optionnel declenche a la demande du client sur un produit. |
|
| 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 (Counter/Drive) -> Consulter le catalogue | include | L'equipier consulte le catalogue pour saisir au comptoir/drive. |
|
| 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 detenant la permission requise. |
|
| Cas back-office -> S'authentifier | include | Acces conditionne a une session authentifiee. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Points resolus par rapport au v0.1
|
## 6. Incoherences remontees vers les autres livrables
|
||||||
|
|
||||||
Les incoherences que le v0.1 remontait pour arbitrage sont desormais tranchees
|
Ces ecarts entre les sources sont signales pour arbitrage de l'auteur (la
|
||||||
par le modele v0.2 (`dictionary.md`, `mct.md`).
|
modelisation finale releve de sa decision, mantra de validation humaine).
|
||||||
|
|
||||||
1. **Acteur "Caisse"** : ecarte. L'encaissement est atomique a la creation
|
1. **ENUM `statut` et phase de paiement (tranche)**
|
||||||
(saisie du numero = substitut de paiement, `mct.md` section 13) et realise
|
Le dictionnaire (`dictionary.md` 3.5) definit
|
||||||
par le Customer (kiosk) ou par `counter`/`drive` (back-office). Aucun role
|
`statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled')`
|
||||||
`caisse` n'est necessaire.
|
avec un paiement explicite. La regle metier confirmee fixe deux phases
|
||||||
2. **"Manager" vs "Admin"** : scindes en deux roles distincts. `manager` gere le
|
successives, la composition de la commande puis son paiement. Le cas
|
||||||
catalogue (create/update), le stock/reappro et les stats ; `admin` ajoute les
|
"Payer la commande" est donc retenu cote Client et materialise la transition
|
||||||
suppressions catalogue, la gestion des utilisateurs et le RBAC.
|
`pending_payment -> paid`. Cet ecart est tranche : la machine canonique de
|
||||||
3. **"Accueil" unique** : scinde en `counter` et `drive`, car le tag `source` et
|
`state-commande.md` fait foi.
|
||||||
le filtre `role_visible_source` different selon le canal.
|
|
||||||
4. **Machine a etats** : alignee sur les 4 etats du `dictionary.md` 3.10
|
2. **Acteur "Caisse" absent du RBAC**
|
||||||
(`pending_payment -> paid -> delivered` + `cancelled`). Plus de `preparing` /
|
Aucun role `caisse` n'existe (`PROJECT_CONTEXT.md` section 7 : `admin`,
|
||||||
`ready` : la cuisine (`kitchen`) est en lecture seule, la remise est un geste
|
`preparation`, `accueil`). La fonction d'encaissement de la consigne a ete
|
||||||
unique (`counter`/`drive`).
|
rattachee a l'acteur **Accueil**. A confirmer.
|
||||||
5. **Modele permission-driven** : chaque cas back-office est rattache a sa
|
|
||||||
permission (catalogue de 23 codes fige, `dictionary.md` 3.17). Le diagramme
|
3. **"Manager" vs "Admin"**
|
||||||
reflete la matrice seed ; le gardien effectif reste la permission.
|
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