docs: remediation audit vague 1 - sync Merise + journal/ADR + claims faux (#106)
All checks were successful
CI / secret-scan (push) Successful in 20s
CI / php-lint (push) Successful in 45s
CI / static-tests (push) Successful in 1m23s
CI / js-tests (push) Successful in 1m5s

This commit is contained in:
Corentin JOGUET 2026-06-25 10:02:13 +02:00
parent 2fe192452d
commit 2e0d535b58
20 changed files with 488 additions and 33 deletions

View file

@ -19,7 +19,7 @@ d'authentification durci dans `docs/uml/security-sequence.md`.
| Brute-force | double throttle : compteur par compte (`user`) + par IP (`login_throttle`), backoff degressif | | Brute-force | double throttle : compteur par compte (`user`) + par IP (`login_throttle`), backoff degressif |
| Sessions | cookies `HttpOnly` + `Secure` + `SameSite=Strict`, regeneration d'ID a la connexion (anti-fixation), idle 4h / absolu 10h | | Sessions | cookies `HttpOnly` + `Secure` + `SameSite=Strict`, regeneration d'ID a la connexion (anti-fixation), idle 4h / absolu 10h |
| Injection | PDO prepared statements exclusivement | | Injection | PDO prepared statements exclusivement |
| Upload | validation MIME + taille, stockage hors webroot | | Upload | non implemente (aucun flux d'upload livre) ; prevu : validation MIME + taille, stockage hors webroot |
| En-tetes / PHP | `expose_php=Off`, `allow_url_fopen/include=Off`, `cgi.fix_pathinfo=0`, fonctions d'execution systeme desactivees | | En-tetes / PHP | `expose_php=Off`, `allow_url_fopen/include=Off`, `cgi.fix_pathinfo=0`, fonctions d'execution systeme desactivees |
| RGPD | retention limitee (audit ~12 mois, throttle 24h, commandes ~3 ans), droit consultation/modif/suppression | | RGPD | retention limitee (audit ~12 mois, throttle 24h, commandes ~3 ans), droit consultation/modif/suppression |
| Secrets | `.env` gitignore, tenu hors de `.git/config` (credential helper lisant `.env`), secret-scan gitleaks en CI | | Secrets | `.env` gitignore, tenu hors de `.git/config` (credential helper lisant `.env`), secret-scan gitleaks en CI |

View file

@ -17,13 +17,17 @@ Wakdo simule une borne de commande tactile de restauration rapide, avec back-off
d'administration, workflow cuisine et API REST interne. Deux surfaces applicatives : d'administration, workflow cuisine et API REST interne. Deux surfaces applicatives :
- **Borne (kiosk)** — front statique (HTML/CSS/JS vanilla ES6) servi par Apache, - **Borne (kiosk)** — front statique (HTML/CSS/JS vanilla ES6) servi par Apache,
consommant des donnees (JSON statique en P5, API DB-backed au swap P4). consommant l'API REST DB-backed (`/api/*`). Le repli JSON statique initial a ete
retire au profit d'un branchement direct sur l'API.
- **Back-office + API** — application PHP rendue serveur (MVC maison) + endpoints - **Back-office + API** — application PHP rendue serveur (MVC maison) + endpoints
`/api/*`, derriere authentification et RBAC. `/api/*`, derriere authentification et RBAC.
Trois canaux de commande (`source`) : `kiosk`, `counter`, `drive`. Le cycle de vie Trois canaux de commande (`source`) : `kiosk`, `counter`, `drive`. Le cycle de vie
d'une commande et la machine a etats sont decrits dans `docs/merise/` (domaine d'une commande et la machine a etats sont decrits dans `docs/merise/`. Le domaine
commande = phase **P4**, schema en base mais workflow applicatif a venir). commande est livre de bout en bout : creation et encaissement via l'API
(`POST /api/orders`, `POST /api/orders/{number}/pay` avec decrement de stock),
file cuisine (KDS), annulation et livraison cote back-office, saisie comptoir et
drive (POS tactile).
--- ---
@ -132,8 +136,8 @@ src/app/
Views/ admin/* (pages back-office rendues serveur), auth/* (login/reset) Views/ admin/* (pages back-office rendues serveur), auth/* (login/reset)
src/public/ src/public/
admin/ front controller + assets (CSS/JS) du back-office admin/ front controller + assets (CSS/JS) du back-office
borne/ front kiosk statique (index, categories, products, product, cart, borne/ front kiosk statique (index, categories, products, payment,
payment, confirmation) + assets JS modules + data JSON confirmation ; panier en panneau persistant) + assets JS modules
``` ```
Conventions transverses : controleurs non-`final` (seam de test : sous-classe injectant Conventions transverses : controleurs non-`final` (seam de test : sous-classe injectant
@ -165,7 +169,7 @@ Vue rendue dans admin/layout (sorties echappees, RG-T15) | ou JSON pour /api/*
``` ```
La borne (kiosk) est servie en statique par Apache ; ses pages consomment les donnees La borne (kiosk) est servie en statique par Apache ; ses pages consomment les donnees
via `fetch` (JSON statique en P5 ; bascule sur `/api/*` DB-backed au swap P4). via `fetch` sur l'API DB-backed (`/api/*`).
--- ---
@ -214,7 +218,7 @@ Threat model STRIDE + classification des donnees : `docs/PROJECT_CONTEXT.md` sec
`ingredient`, `product_ingredient`, `allergen`, `ingredient_allergen`, `stock_movement`. `ingredient`, `product_ingredient`, `allergen`, `ingredient_allergen`, `stock_movement`.
- **RBAC / comptes** : `user`, `role`, `permission`, `role_permission`, - **RBAC / comptes** : `user`, `role`, `permission`, `role_permission`,
`role_visible_source`. `role_visible_source`.
- **Commande (P4, schema pret)** : `customer_order`, `order_item`, - **Commande (livre)** : `customer_order`, `order_item`,
`order_item_selection`, `order_item_modifier`. `order_item_selection`, `order_item_modifier`.
- **Transverses** : `audit_log` (journal immuable), `login_throttle`, `pin_throttle`. - **Transverses** : `audit_log` (journal immuable), `login_throttle`, `pin_throttle`.

View file

@ -117,7 +117,7 @@ Client Borne (Bloc 1) API (Bloc 2) BDD
### Compatibilite evaluation par bloc ### Compatibilite evaluation par bloc
- **Jury Bloc 1** : voit le front seul ; le front peut tomber en fallback sur JSON statiques fournis (`src/public/borne/data/*.json`) si l'API est indisponible. - **Jury Bloc 1** : voit le front seul ; le front consomme les donnees via `fetch` sur l'API (`/api/*`). Le fallback JSON statique initialement envisage a ete retire (la borne est branchee directement sur l'API DB-backed).
- **Jury Bloc 2** : voit le back-office + teste l'API via curl/Postman de maniere autonome, sans dependre du front. - **Jury Bloc 2** : voit le back-office + teste l'API via curl/Postman de maniere autonome, sans dependre du front.
- **Jury Bloc 5** : lance `docker compose up` ou `docker compose up`, verifie la CI/CD, les crons, l'archi, les scripts. - **Jury Bloc 5** : lance `docker compose up` ou `docker compose up`, verifie la CI/CD, les crons, l'archi, les scripts.
@ -205,7 +205,7 @@ Reseaux :
### Bloc 1 — Borne client (Front) ### Bloc 1 — Borne client (Front)
**IN scope :** **IN scope :**
- Affichage dynamique menus + produits (charges par Ajax depuis API ou JSON fallback) - Affichage dynamique menus + produits (charges par `fetch` depuis l'API `/api/*`)
- Composition panier : produits unitaires OU menus (burger + accompagnement + boisson + sauce) - Composition panier : produits unitaires OU menus (burger + accompagnement + boisson + sauce)
- Options taille (normale / grande, +0,50 € sur grande) pour accompagnements et boissons - Options taille (normale / grande, +0,50 € sur grande) pour accompagnements et boissons
- Options de personnalisation simples (ex : sans oignon, avec fromage) - Options de personnalisation simples (ex : sans oignon, avec fromage)
@ -232,7 +232,7 @@ Reseaux :
- **Manager** : catalogue (create/update), stock (reappro + inventaire), statistiques ; utilisateurs en **lecture seule** (`user.read`, pas de creation/modification/desactivation), pas d'acces RBAC - **Manager** : catalogue (create/update), stock (reappro + inventaire), statistiques ; utilisateurs en **lecture seule** (`user.read`, pas de creation/modification/desactivation), pas d'acces RBAC
- **Kitchen** : file des commandes `paid` triee par `paid_at` croissant, en **lecture seule** (KDS visuel) ; inventaire - **Kitchen** : file des commandes `paid` triee par `paid_at` croissant, en **lecture seule** (KDS visuel) ; inventaire
- **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 - **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 : **non implemente** ; prevu (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)

View file

@ -0,0 +1,43 @@
# ADR-0011 — POS tactile a tuiles pour la saisie comptoir/drive
- Statut : Accepte
- Date : 2026-06-25
## Contexte
La saisie de commande comptoir/drive (mlt 4.1, `CREATE_COUNTER_ORDER`) avait d'abord ete
livree comme formulaire-liste enrichi (#100) : une liste de produits avec champs de
quantite, prix, verrou du mode de service au canal drive, file des commandes recentes.
Cet ecran est destine a des equipiers non-techniques, sur tablette, dans un contexte de
caisse ou la rapidite compte. Le formulaire-liste se prete mal au tactile (petites cibles,
defilement) et ne ressemble pas au geste deja appris cote borne client. Options :
conserver et raffiner le formulaire-liste ; adopter un paradigme de caisse (POS) a tuiles
reutilisant l'UX borne ; integrer une bibliotheque de POS tierce.
## Decision
La saisie comptoir/drive devient un **POS tactile a tuiles** (#104) calque sur la borne
client : onglets categories en haut, grille de tuiles produits/menus, panneau commande
persistant a droite ; un tap ajoute le produit, un produit a modificateurs ou un menu
ouvre une modale de composition. Le panier est construit cote client en vanilla JS
CSP-safe (`'self'`, zero handler inline) a partir d'un script JSON inerte, puis serialise
dans un champ cache `items_json`. Le **serveur reste seul juge** : il revalide la forme
(RG-T18), recalcule les prix et resout les modificateurs (RG-T16) ; les prix affiches cote
client sont indicatifs. Un **seul controleur** sert les deux canaux, la `source`
(counter/drive) etant derivee du chemin de la requete.
## Consequences
- (+) Geste unifie borne/comptoir : moins d'apprentissage, reutilisation des patterns
composeur deja eprouves cote borne.
- (+) Cibles tactiles larges adaptees a la tablette ; zero jargon dev a l'ecran
(utilisateurs non-techniques).
- (+) Decoupage par chemin (`/drive...` vs `/counter...`) : les canaux restent etanches,
un equipier ne peut pas requalifier sa commande via un champ falsifie.
- (+) Sans framework front (coherent ADR-0002) ; coherent avec ADR-0001 (pas de
dependance tierce, donc pas de POS externe).
- (-) Le POS est interactif par nature : sans JS, la grille ne s'affiche pas (un message
invite a activer JS). Un repli legacy `qty_<id>` reste accepte quand `items_json` est
absent, mais l'experience cible suppose JS.
- (-) Cet ecran a ete refondu deux fois a court intervalle (#100 puis #104) ; le palier
#100 est assume comme etape, pas comme dette cachee.
- Fichiers : `src/app/Controllers/CounterOrderController.php`,
`src/app/Views/admin/counter/new.php`, `src/public/admin/assets/js/counter-order.js`.
Detail : journal `docs/journal/2026-06-25--audit-remediation-et-features-94-105.md`.

View file

@ -0,0 +1,41 @@
# ADR-0012 — Page Stock en tableau de bord (alertes + reapprovisionnement en avant)
- Statut : Accepte
- Date : 2026-06-25
## Contexte
La page d'accueil Ingredients/Stock (domaine 8.8 + 9) etait une liste-CRUD exhaustive :
tous les ingredients, avec les actions creer/modifier/supprimer en premier plan. Elle
etait jugee trop chargee et opaque pour un equipier non-technique. Le besoin metier
quotidien est de voir vite ce qui manque et de reapprovisionner ; l'edition de fiches est
rare. De plus, le lien entre stock et disponibilite borne (un ingredient requis sous le
seuil critique rend indisponibles les produits qui l'utilisent, RG-T21, cf. ADR-0003)
n'etait pas visible a l'ecran. Options : garder la liste exhaustive triable ; basculer
vers un tableau de bord oriente action ; deux pages distinctes (dashboard + gestion).
## Decision
La page Stock devient un **tableau de bord oriente action** (#105) : un bandeau explicite
le lien stock -> disponibilite borne (RG-T21) ; un resume compte les ingredients
critiques / en alerte / au-dessus du seuil ; une section "A reapprovisionner" met en avant
les ingredients bas (critiques d'abord) avec barre de niveau et bouton de
reapprovisionnement direct. La liste complete passe au second plan et le CRUD est relegue.
Les compteurs par etat sont **calcules cote serveur** dans `IngredientController::index()`
a partir du `stock_band` deja resolu par le depot, pour garder la vue declarative et la
valeur directement testable. Les sous-pages (reappro, inventaire, mouvements, creation)
restent inchangees.
## Consequences
- (+) L'ecran s'aligne sur le geste quotidien (reperer le manque, reapprovisionner) plutot
que sur l'edition de fiches.
- (+) Le lien stock -> disponibilite borne (RG-T21) devient explicite a l'ecran, pas
seulement dans le code.
- (+) Compteurs testables (la logique de comptage est cote serveur, pas dans la vue) :
+4 cas de test `IngredientController` (bandeau, promotion d'un critique en section
reappro, etat vide positif, compteurs par etat).
- (-) La liste exhaustive et le CRUD sont moins immediats (un cran plus loin) : choix
assume, ces operations etant moins frequentes que la lecture d'alerte.
- (-) Le tri/filtre avance de l'ancienne liste n'est pas reconduit en premier plan.
- Coherent avec ADR-0002 (MVC rendu serveur) et ADR-0003 (disponibilite calculee depuis
le stock). Fichiers : `src/app/Controllers/IngredientController.php`,
`src/app/Views/admin/ingredients/index.php`, `src/public/admin/assets/css/admin.css`.
Detail : journal `docs/journal/2026-06-25--audit-remediation-et-features-94-105.md`.

View file

@ -18,6 +18,8 @@ une decision revisee donne une nouvelle fiche qui *supersede* l'ancienne (statut
| [0008](0008-makefile-vers-compose-migrate.md) | Du Makefile a `docker compose up` (service wakdo-migrate) | Accepte | | [0008](0008-makefile-vers-compose-migrate.md) | Du Makefile a `docker compose up` (service wakdo-migrate) | Accepte |
| [0009](0009-compose-standalone-et-prod-gitignore.md) | docker-compose.yml standalone + docker-compose.prod.yml gitignore | Accepte | | [0009](0009-compose-standalone-et-prod-gitignore.md) | docker-compose.yml standalone + docker-compose.prod.yml gitignore | Accepte |
| [0010](0010-cookie-secure-conditionnel-https.md) | Cookie de session Secure conditionnel au HTTPS | Accepte | | [0010](0010-cookie-secure-conditionnel-https.md) | Cookie de session Secure conditionnel au HTTPS | Accepte |
| [0011](0011-pos-tactile-tuiles-comptoir-drive.md) | POS tactile a tuiles pour la saisie comptoir/drive | Accepte |
| [0012](0012-page-stock-tableau-de-bord.md) | Page Stock en tableau de bord (alertes + reappro en avant) | Accepte |
## Modele de fiche ## Modele de fiche

View file

@ -0,0 +1,210 @@
# 2026-06-25 — Synthese : CD prod, SMTP reel, durcissement borne, POS comptoir/drive, dashboard stock (#94-#105)
**Auteur : BYAN.** Retrospective de synthese couvrant douze PR mergees apres la session
du 2026-06-18 (front login + amorce P4 commande). Elles se regroupent en quatre fils :
mise en production reelle (#94-#97), finition du parcours borne client (#98, #99, #101,
#102, #103), et refonte de la saisie comptoir/drive et de la page stock cote back-office
(#100, #104, #105). Entree descriptive : on decrit ce qui est livre, pas ce qui est
promis.
---
## Ce qui a ete livre (PR mergees)
| PR | Bloc | Objet |
|----|------|-------|
| #94 | CD | Deploiement push-based vers Vision (prod) + preuve de version dans `GET /api/health` |
| #95 | CD | Modeles versionnes `docker-compose.prod.yml.example` + `.env.prod.example` |
| #96 | Auth | Envoi reel de l'email de reset via relais SMTP (Brevo) — client SMTP maison |
| #97 | CD | Passage des variables SMTP/MAIL au conteneur `wakdo-app` (correctif #96) |
| #98 | Borne | Menu Maxi agrandit la boisson en 50cl + transport du format choisi |
| #99 | Borne | Produit/menu en rupture de stock rendu non commandable (RG-T21) |
| #100 | Back-office | Refonte saisie comptoir/drive : prix, verrou du mode, navigation, file |
| #101 | Borne | Panier unique = panneau persistant (retrait de `cart.html` et `product.html`) |
| #102 | Borne | Confirmation avant l'abandon de la commande |
| #103 | Borne | Bascule des allergenes sur `/api/allergens` + menage des donnees/docs statiques |
| #104 | Back-office | Saisie comptoir/drive en POS tactile a tuiles (refonte de #100) |
| #105 | Back-office | Page Stock en tableau de bord (alertes + reapprovisionnement en avant) |
---
## Bloc 1 — Mise en production reelle (#94, #95, #96, #97)
### Ce qui a ete fait
- **CD push-based (#94)** : `.forgejo/workflows/deploy.yml` ouvre, sur push `main`, une
session SSH vers Vision (l'hote de prod). `scripts/deploy.sh` y recupere `main` en
fast-forward, ecrit un marqueur de version (`src/VERSION` : SHA + date), journalise une
ligne dans `deploy.log`, puis reconstruit et recree la stack. `GET /api/health` expose
desormais `version` et `deployed_at`, lus depuis ce marqueur : c'est la preuve cote app
qu'un deploiement a bien repris le dernier commit. Doc : `docs/architecture/deployment.md`.
- **Modeles de prod versionnes (#95)** : `docker-compose.prod.yml.example` et
`.env.prod.example` entrent au depot comme gabarits. Le fichier reel reste gitignore
(specifique a l'hote : Traefik, reseau externe), conformement a ADR-0009 ; le `.example`
documente la forme attendue.
- **SMTP reel (#96, #97)** : la reinitialisation de mot de passe envoyait jusque-la un mail
inerte. Un client SMTP maison (`SmtpClient`, `SmtpMailer`, transport via flux PHP
`StreamSmtpTransport` derriere l'interface `SmtpTransport`) parle a un relais reel
(Brevo en l'occurrence). `PasswordResetController` s'y branche. #97 corrige un oubli :
les variables SMTP/MAIL n'etaient pas transmises au conteneur `wakdo-app` (declarees
dans les deux fichiers compose).
### Pourquoi — decisions et alternatives
- **Decision : CD par SSH, pas par Docker-in-CI.** Le runner Forgejo (sur Stark) n'a pas
acces au socket Docker, par choix de securite : un job CI ne pilote pas Docker sur son
hote. Le deploiement vers Vision se fait donc par SSH avec une *forced command* cote
serveur. *Alternative ecartee* : donner le socket Docker au runner — rejetee pour la
surface d'attaque. C'est le prolongement de la decision E2E-CI du 2026-06-18 (meme
contrainte de socket).
- **Decision : client SMTP maison plutot qu'une bibliotheque.** ADR-0001 fige le projet
sans Composer ni dependance tierce ; un client SMTP minimal (EHLO/AUTH/MAIL/RCPT/DATA
sur flux) reste coherent avec cette contrainte et reste testable via un faux transport
(`FakeSmtpTransport`). *Alternative ecartee* : `mail()` de PHP — sans relais
authentifie, la delivrabilite est aleatoire et la configuration sort du depot.
- **Decision : marqueur de version dans `src/VERSION` lu a chaud.** Le marqueur est sous
le mount du code (`./src` -> `/var/www/html`), donc relu sans rebuild. Cela donne une
preuve de deploiement observable de l'exterieur sans instrumentation supplementaire.
### Criteres RNCP couverts
- **Bloc 3 - deploiement** : chaine de livraison continue tracable (`deploy.yml`,
`deploy.sh`, `deployment.md`), preuve de version exposee.
- **Bloc 1 - securite** : separation runner/prod sans socket Docker ; secrets de prod
hors du depot (gabarits `.example` seulement).
---
## Bloc 2 — Finition du parcours borne client (#98, #99, #101, #102, #103)
### Ce qui a ete fait
- **Menu Maxi -> boisson 50cl (#98)** : choisir le format Maxi d'un menu fait passer la
boisson de 33cl a 50cl. La migration `0007_product_size_variant` et le seed
`0006_drink_maxi_variant` portent la variante en base ; le format choisi est transporte
jusqu'au paiement (`checkout.js`, `page-product-menu.js`). Tests `OrderRepository` +
tests JS du composeur.
- **Rupture non commandable (#99, RG-T21)** : un produit (ou un menu dont un composant
requis) sous le seuil critique de stock est marque indisponible et non ajoutable a la
borne ; le serveur refuse aussi la creation cote `OrderRepository` (defense en
profondeur, le client n'est pas seul juge). Visuel d'indisponibilite cote `style.css`.
- **Panier unique = panneau persistant (#101)** : suppression des pages `cart.html` et
`product.html` ; le panier devient un panneau lateral persistant (`order-panel.js`)
present sur les pages au lieu d'une page dediee. Le net du diff est negatif
(-784 lignes) : c'est une simplification de l'architecture front borne.
- **Confirmation d'abandon (#102)** : abandonner la commande ouvre une modale de
confirmation (`confirm-modal.js`) au lieu de vider le panier au premier clic.
- **Allergenes via API (#103)** : la borne lisait des fichiers JSON statiques
(`allergens.json`, `categories.json`, `produits.json`) ; elle consomme desormais
`/api/allergens` (et le catalogue par API). Les JSON statiques et la doc afferente sont
retires (menage). `docs/api/conventions.md` et `docs/design/maquette-vs-build.md` mis a
jour.
### Pourquoi — decisions et alternatives
- **Decision : rupture controlee cote serveur ET cote client (#99).** L'indisponibilite
est calculee au plus pres de la verite (RG-T21, ADR-0003) ; le client la reflete pour
l'UX, mais `OrderRepository` revalide a la creation. *Alternative ecartee* : masquer
cote client seulement — laisse une fenetre ou une commande forgee passerait.
- **Decision : panier persistant plutot que page panier (#101).** Sur une borne, l'aller-
retour vers une page panier ajoute une etape ; un panneau visible en permanence reduit
la navigation. Le retrait de deux pages reduit aussi la surface a maintenir.
- **Decision : source de verite unique pour les donnees borne (#103).** Les JSON statiques
dupliquaient le catalogue de la base ; ils pouvaient diverger silencieusement. Passer par
l'API supprime la duplication et fait converger borne et back-office sur le meme modele.
### Criteres RNCP couverts
- **Bloc 2 - front client** : parcours borne complet (composeur, panier, abandon,
indisponibilite) sur donnees reelles par API.
- **Bloc 1 - regles metier** : RG-T21 (disponibilite calculee) appliquee de bout en bout.
---
## Bloc 3 — Saisie comptoir/drive en POS tactile (#100 puis #104)
### Ce qui a ete fait
- **#100** a d'abord refondu la saisie comptoir/drive en tant que formulaire enrichi :
prix affiches, verrou du mode de service au canal drive, navigation, file des commandes
recentes. `CounterOrderController` derive la `source` (counter/drive) du chemin de la
requete ; ajout de la liste des commandes recentes par canal.
- **#104** a ensuite remplace ce formulaire-liste par un **POS tactile a tuiles** facon
borne : onglets categories en haut, grille de tuiles produits/menus, panneau commande
persistant a droite. Pense pour la tablette (grandes cibles, un tap = ajout). Le panier
est construit cote client (`counter-order.js`, CSP `'self'`, vanilla, zero handler
inline) a partir d'un script JSON inerte, puis serialise dans un champ cache
`items_json`. Le serveur revalide la forme (RG-T18), recalcule les prix (RG-T16) et
resout les modificateurs : les prix cote client sont indicatifs.
### Pourquoi — decisions et alternatives
- **Decision : POS a tuiles, reutilisant l'UX borne (#104).** Cf. ADR-0011. L'equipier
comptoir et le client borne font le meme geste (choisir des produits, composer) ; un
meme paradigme tuiles+panneau reduit l'apprentissage et reutilise les patterns deja
eprouves. *Alternative ecartee* : garder le formulaire-liste (#100) — moins adapte au
tactile et a la rapidite attendue d'une caisse.
- **Decision : serveur seul juge des prix et de la composition.** Le client propose, le
serveur fige (RG-T16). Coherent avec le reste du domaine commande.
- **Decision : un controleur pour deux canaux, source derivee du chemin.** Le decoupage
par chemin (`/drive...` vs `/counter...`) plutot que par parametre rend les deux canaux
etanches : un equipier ne peut pas requalifier sa commande en falsifiant un champ.
### Comment — points techniques cles
- `CounterOrderController` : `source()` lue depuis le chemin ; `store()` decode
`items_json`, revalide, delegue a `createStaffOrder` (commande creee directement `paid`,
encaissement immediat, sans PIN — la permission `order.create` suffit). Repli legacy
`qty_<id>` accepte quand `items_json` est absent (degradation sans JS).
- `counter-order.js` : construction des onglets/grille/panneau, modale de composition pour
produits a modificateurs et menus, serialisation a la soumission.
### Criteres RNCP couverts
- **Bloc 2 - back-office** : ecran de caisse pour equipiers non-techniques (zero jargon),
CSP-safe sans framework front.
- **Bloc 1 - regles metier** : recalcul/revalidation serveur (RG-T16, RG-T18), etancheite
des canaux.
---
## Bloc 4 — Page Stock en tableau de bord (#105)
### Ce qui a ete fait
La page d'accueil Ingredients/Stock, jugee trop chargee et opaque, est refondue en
tableau de bord. Elle porte desormais : un bandeau expliquant le lien stock ->
disponibilite borne (un ingredient requis sous le seuil critique rend indisponibles les
produits qui l'utilisent, RG-T21) ; un resume comptant les ingredients critiques / en
alerte / au-dessus du seuil ; une section "A reapprovisionner" mettant en avant les
ingredients bas (critiques d'abord) avec barre de niveau et bouton de reapprovisionnement
direct. La liste complete passe au second plan et le CRUD est relegue. Les sous-pages
(reappro, inventaire, mouvements, creation) restent inchangees. `index()` expose des
compteurs par etat, calcules cote serveur a partir de `stock_band` deja resolu par le
depot, pour garder la vue declarative et la valeur testable.
### Pourquoi — decisions et alternatives
Cf. ADR-0012. **Decision : dashboard oriente action plutot que liste-CRUD.** Le metier
quotidien d'un equipier stock est de voir vite ce qui manque et de reapprovisionner, pas
d'editer des fiches. Mettre les alertes et le bouton de reappro en avant aligne l'ecran
sur ce geste. *Alternative ecartee* : garder une liste exhaustive triable — exhaustive
mais muette sur l'urgence.
### Criteres RNCP couverts
- **Bloc 2 - back-office** : ergonomie orientee tache pour utilisateur non-technique.
- **Bloc 1 - regles metier** : lien stock -> disponibilite (RG-T21) rendu explicite a
l'ecran.
---
## Verifications
A la cloture du lot : suite JS 135 tests verte (verifiee en local), suites PHPUnit et PHPStan niveau 6 vertes en CI Forgejo,
`php -l` propre. Chaque PR est passee par la CI Forgejo (checks requis) avant merge natif.
## Points d'amelioration conscients
- **#100 puis #104** : la refonte POS (#104) remplace une premiere refonte (#100) du meme
ecran a quelques jours d'intervalle. Le formulaire enrichi de #100 a servi de palier
avant le pivot tactile ; l'iteration est assumee plutot que masquee.
- **SMTP** : le client maison couvre le cas d'usage reset (un destinataire, texte). Il
n'est pas un agent mail generaliste ; tout besoin plus large (pieces jointes, files)
serait a reevaluer.
## Liens vers artefacts
- Commits : `8c5d942` (#94) -> `03ef99d` (#105).
- ADR associes : `docs/adr/0011-pos-tactile-tuiles-comptoir-drive.md`,
`docs/adr/0012-page-stock-tableau-de-bord.md`.
- Fichiers principaux : `.forgejo/workflows/deploy.yml`, `scripts/deploy.sh`,
`src/app/Controllers/HealthController.php`, `src/app/Auth/SmtpClient.php`,
`src/app/Controllers/CounterOrderController.php`,
`src/public/admin/assets/js/counter-order.js`,
`src/app/Controllers/IngredientController.php`,
`src/app/Views/admin/ingredients/index.php`.

View file

@ -33,8 +33,13 @@ Les fichiers sont ordonnes chronologiquement par leur nom.
| 2026-06-04 | [conception-prodlike-revision](2026-06-04--conception-prodlike-revision.md) | Revue d'alignement P1 + decisions prod-like du modele de donnees (drop commande_event, nommage EN, TVA par produit apres fact-check BOFiP, perso menus/ingredients, allergenes, ~16 entites) | `feat/p1-conception` | | 2026-06-04 | [conception-prodlike-revision](2026-06-04--conception-prodlike-revision.md) | Revue d'alignement P1 + decisions prod-like du modele de donnees (drop commande_event, nommage EN, TVA par produit apres fact-check BOFiP, perso menus/ingredients, allergenes, ~16 entites) | `feat/p1-conception` |
| 2026-06-15 | [p3-throttle-pin-rg-t22](2026-06-15--p3-throttle-pin-rg-t22.md) | P3 securite : throttle du PIN d'action sensible (RG-T22) — design multi-agents + verification adversariale, dimension "utilisateur agissant", entite 22 `pin_throttle` | `feat/p3-pin-throttle` -> `dev` | | 2026-06-15 | [p3-throttle-pin-rg-t22](2026-06-15--p3-throttle-pin-rg-t22.md) | P3 securite : throttle du PIN d'action sensible (RG-T22) — design multi-agents + verification adversariale, dimension "utilisateur agissant", entite 22 `pin_throttle` | `feat/p3-pin-throttle` -> `dev` |
| 2026-06-16 | [audit-reel-livrables-p2-p3](2026-06-16--audit-reel-livrables-p2-p3.md) | Verification sur pieces des livrables du 2026-06-15 (sweep 10 dimensions + adversarial) : socle SbD confirme, miss confirmes par gravite (php.ini non deploye, CI sans tests DB, XSS kiosk, liens morts...) et remediations | `docs/journal-audit-2026-06-16` -> `dev` (PR #19/#20/#21) | | 2026-06-16 | [audit-reel-livrables-p2-p3](2026-06-16--audit-reel-livrables-p2-p3.md) | Verification sur pieces des livrables du 2026-06-15 (sweep 10 dimensions + adversarial) : socle SbD confirme, miss confirmes par gravite (php.ini non deploye, CI sans tests DB, XSS kiosk, liens morts...) et remediations | `docs/journal-audit-2026-06-16` -> `dev` (PR #19/#20/#21) |
| 2026-06-04 | [p1-merise-v0.2-rewrite-and-forgejo-migration](2026-06-04--p1-merise-v0.2-rewrite-and-forgejo-migration.md) | P1 Merise v0.2 (prod-like) reecrit + migration vers Forgejo auto-heberge | `feat/p1-conception` |
| 2026-06-17 | [makefile-to-compose-migrate](2026-06-17--makefile-to-compose-migrate.md) | Du Makefile a `docker compose up` : service one-shot `wakdo-migrate` (migrate + seed idempotents) | `feat/compose-migrate` -> `dev` |
| 2026-06-17 | [session-infra-doc-e2e](2026-06-17--session-infra-doc-e2e.md) | Session infra compose, documentation Forge, amorce des tests E2E Playwright | `dev` (PR #37/#38/#39 et suivantes) |
| 2026-06-18 | [front-login-ui-admin-p4-commande](2026-06-18--front-login-ui-admin-p4-commande.md) | Page login, refonte UI admin (equipiers non-techniques), humanisation des libelles, amorce P4 commande (creation + encaissement) | `dev` (PR #48 a #58) |
| 2026-06-25 | [audit-remediation-et-features-94-105](2026-06-25--audit-remediation-et-features-94-105.md) | Synthese #94-#105 : CD push-based vers Vision + preuve de version, modeles compose prod, SMTP reel reset (Brevo), durcissement borne (Maxi 50cl, rupture non commandable, panier persistant, confirm abandon, allergenes /api), POS tactile comptoir/drive, page Stock en tableau de bord | `dev`/`main` (PR #94 a #105) |
*Mis a jour a chaque nouvelle entree.* *Mis a jour a chaque nouvelle entree. Les entrees sont ordonnees par leur nom de fichier (date) ; cet index les liste dans l'ordre de redaction.*
--- ---

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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