docs(merise): synchronise le referentiel avec la base deployee (5 migrations additives + order_number reel)
This commit is contained in:
parent
03ef99d67b
commit
10440bb266
12 changed files with 171 additions and 21 deletions
|
|
@ -13,6 +13,9 @@ erDiagram
|
|||
varchar name
|
||||
text description
|
||||
int price_cents
|
||||
int maxi_variant_product_id FK
|
||||
smallint size_cl
|
||||
int base_product_id FK
|
||||
smallint vat_rate
|
||||
varchar image_path
|
||||
tinyint is_available
|
||||
|
|
@ -46,6 +49,8 @@ erDiagram
|
|||
category ||--o{ product : "groups"
|
||||
category ||--o{ menu : "groups"
|
||||
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_slot ||--o{ menu_slot_option : "lists"
|
||||
product ||--o{ menu_slot_option : "is_eligible_for"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ erDiagram
|
|||
int stock_capacity
|
||||
smallint pack_size
|
||||
varchar pack_label
|
||||
smallint energy_kcal_100g
|
||||
varchar nutrition_source
|
||||
datetime nutrition_fetched_at
|
||||
smallint low_stock_pct
|
||||
smallint critical_stock_pct
|
||||
tinyint is_active
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ erDiagram
|
|||
enum source
|
||||
int acting_user_id FK
|
||||
enum service_mode
|
||||
varchar service_tag
|
||||
enum status
|
||||
int total_ht_cents
|
||||
int total_vat_cents
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ erDiagram
|
|||
int category_id FK
|
||||
varchar name
|
||||
int price_cents
|
||||
int maxi_variant_product_id FK
|
||||
smallint size_cl
|
||||
int base_product_id FK
|
||||
smallint vat_rate
|
||||
tinyint is_available
|
||||
smallint display_order
|
||||
|
|
@ -41,6 +44,8 @@ erDiagram
|
|||
category ||--o{ product : "category_id (RESTRICT)"
|
||||
category ||--o{ menu : "category_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_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)"
|
||||
product ||--o{ menu_slot_option : "product_id (RESTRICT)"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ erDiagram
|
|||
int stock_quantity
|
||||
int stock_capacity
|
||||
smallint pack_size
|
||||
smallint energy_kcal_100g
|
||||
varchar nutrition_source
|
||||
datetime nutrition_fetched_at
|
||||
smallint low_stock_pct
|
||||
smallint critical_stock_pct
|
||||
tinyint is_active
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ erDiagram
|
|||
enum source
|
||||
int acting_user_id FK
|
||||
enum service_mode
|
||||
varchar service_tag
|
||||
enum status
|
||||
int total_ht_cents
|
||||
int total_vat_cents
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
**Date** : 2026-06-04 (ajouts security-by-design 2026-06-11)
|
||||
**Branche** : `feat/p1-conception`
|
||||
**Statut** : prod-like — toutes les decisions D1-D8 + stock appliquees (voir `docs/notes/revue-alignement-p1.md` §7) ; couche security-by-design 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)
|
||||
|
||||
---
|
||||
|
|
@ -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` |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `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 |
|
||||
|
|
@ -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 |
|
||||
| `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") |
|
||||
| `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 d’alerte, 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 |
|
||||
| `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 |
|
||||
|---|---|---|---|---|---|
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `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. |
|
||||
| `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 |
|
||||
|
|
@ -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
|
||||
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
|
||||
par le personnel cuisine et comptoir sans interroger la colonne `source`. Le compteur sequentiel NNN
|
||||
repart chaque jour par canal. Sans collision sur une journee, vu le volume attendu.
|
||||
Rationale : le prefixe encode le canal, utile pour une identification visuelle rapide par le personnel
|
||||
cuisine et comptoir sans interroger la colonne `source`. Le suffixe est l'id sequentiel auto-incremente :
|
||||
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).
|
||||
|
||||
### 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
|
||||
(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
|
||||
|
|
|
|||
|
|
@ -111,6 +111,9 @@ erDiagram
|
|||
varchar name
|
||||
text description
|
||||
int price_cents
|
||||
int maxi_variant_product_id FK
|
||||
smallint size_cl
|
||||
int base_product_id FK
|
||||
smallint vat_rate
|
||||
varchar image_path
|
||||
tinyint is_available
|
||||
|
|
@ -144,6 +147,8 @@ erDiagram
|
|||
category ||--o{ product : "groups"
|
||||
category ||--o{ menu : "groups"
|
||||
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_slot ||--o{ menu_slot_option : "lists"
|
||||
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. |
|
||||
| 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. |
|
||||
| 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
|
||||
|
||||
|
|
@ -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).
|
||||
|
||||
**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
|
||||
|
|
@ -188,6 +197,9 @@ erDiagram
|
|||
int stock_capacity
|
||||
smallint pack_size
|
||||
varchar pack_label
|
||||
smallint energy_kcal_100g
|
||||
varchar nutrition_source
|
||||
datetime nutrition_fetched_at
|
||||
smallint low_stock_pct
|
||||
smallint critical_stock_pct
|
||||
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.
|
||||
|
||||
**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
|
||||
|
|
@ -277,6 +291,7 @@ erDiagram
|
|||
enum source
|
||||
int acting_user_id FK
|
||||
enum service_mode
|
||||
varchar service_tag
|
||||
enum status
|
||||
int total_ht_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
|
||||
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
|
||||
|
|
@ -580,7 +600,7 @@ Le MCD reste au niveau conceptuel. Les decisions suivantes sont reportees au MLD
|
|||
des PK composites.
|
||||
2. **PK technique vs identifiant metier** : `id INT UNSIGNED AUTO_INCREMENT` sur toutes les entites principales.
|
||||
`customer_order` porte en plus `order_number VARCHAR(20) UNIQUE` (lisible par un humain,
|
||||
format `K/C/D-YYYY-MM-DD-NNN` par canal).
|
||||
format prefixe canal + id : `K<id>` / `C<id>` / `D<id>`).
|
||||
3. **Regles ON DELETE** : CASCADE vs RESTRICT vs SET NULL. Detaillees dans le MLD.
|
||||
4. **Contraintes CHECK** : exclusivite de polymorphisme sur `order_item`, contrainte croisee
|
||||
`source/service_mode` sur `customer_order`, invariant arithmetique sur les totaux.
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ Pour chaque operation, le document fournit :
|
|||
| **Synchronisation** | AND (les deux actions requises) |
|
||||
| **Condition** | Le panier contient au moins 1 article. Le numero de commande saisi est non vide. |
|
||||
| **Operation** | CREATE_ORDER |
|
||||
| **Description** | 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) |
|
||||
| **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 |
|
||||
| **Condition** | L'acteur est authentifie et detient la permission `order.create`. La `source` est `counter` ou `drive` (auto-taggee depuis `role.order_source`). |
|
||||
| **Operation** | CREATE_COUNTER_ORDER |
|
||||
| **Description** | 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`) |
|
||||
| **Resultat** | Commande creee (statut `paid`), numero de commande communique au client |
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ erDiagram
|
|||
int category_id FK
|
||||
varchar name
|
||||
int price_cents
|
||||
int maxi_variant_product_id FK
|
||||
smallint size_cl
|
||||
int base_product_id FK
|
||||
smallint vat_rate
|
||||
tinyint is_available
|
||||
smallint display_order
|
||||
|
|
@ -155,6 +158,8 @@ erDiagram
|
|||
category ||--o{ product : "category_id (RESTRICT)"
|
||||
category ||--o{ menu : "category_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_slot ||--o{ menu_slot_option : "menu_slot_id (CASCADE)"
|
||||
product ||--o{ menu_slot_option : "product_id (RESTRICT)"
|
||||
|
|
@ -171,6 +176,9 @@ erDiagram
|
|||
int stock_quantity
|
||||
int stock_capacity
|
||||
smallint pack_size
|
||||
smallint energy_kcal_100g
|
||||
varchar nutrition_source
|
||||
datetime nutrition_fetched_at
|
||||
smallint low_stock_pct
|
||||
smallint critical_stock_pct
|
||||
tinyint is_active
|
||||
|
|
@ -235,6 +243,7 @@ erDiagram
|
|||
enum source
|
||||
int acting_user_id FK
|
||||
enum service_mode
|
||||
varchar service_tag
|
||||
enum status
|
||||
int total_ht_cents
|
||||
int total_vat_cents
|
||||
|
|
@ -410,11 +419,14 @@ Pas de FK. Table racine du sous-domaine Catalogue.
|
|||
### 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)
|
||||
|
||||
PK : id
|
||||
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)
|
||||
CHK : price_cents > 0
|
||||
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 |
|
||||
| `description` | TEXT | YES | Description longue optionnelle |
|
||||
| `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% |
|
||||
| `image_path` | VARCHAR(255) | YES | Chemin relatif depuis la racine publique |
|
||||
| `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
|
||||
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`
|
||||
|
|
@ -529,6 +557,7 @@ Pas d'horodatages. Table de jointure pure.
|
|||
|
||||
```
|
||||
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)
|
||||
|
||||
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 |
|
||||
| `pack_size` | SMALLINT UNSIGNED NOT NULL DEFAULT 1 | NO | Unites par lot de reapprovisionnement |
|
||||
| `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 d’alerte, 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`) |
|
||||
| `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
|
||||
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`
|
||||
|
|
@ -811,7 +848,7 @@ Pas d'horodatages. Table de jointure pure.
|
|||
|
||||
```
|
||||
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,
|
||||
[paid_at], [delivered_at], [cancelled_at],
|
||||
created_at, updated_at)
|
||||
|
|
@ -833,11 +870,12 @@ customer_order (id, order_number, [idempotency_key], source, [#acting_user_id],
|
|||
| Colonne | Type | NULL | Notes |
|
||||
|---|---|---|---|
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `total_ht_cents` | INT UNSIGNED | NO | Snapshot du total HT |
|
||||
| `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` |
|
||||
| `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)
|
||||
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
|
||||
|
|
@ -1235,11 +1284,11 @@ et que toutes les tables se rattachent au MCD.
|
|||
| Entite MCD | Table MLD | Type de mapping | Notes |
|
||||
|---|---|---|---|
|
||||
| `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_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) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
|
|
@ -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) |
|
||||
| `permission` (C13) | `permission` (4.13) | entite 1:1 | |
|
||||
| `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_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) |
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
| **[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-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-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. |
|
||||
|
|
@ -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 |
|
||||
| **[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-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-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. |
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ flowchart LR
|
|||
|
||||
| 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` |
|
||||
| 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` |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue