# Règles de gestion — GestMaquis

Ce document recense, module par module, les règles métier appliquées dans l'application. Il est mis à jour au fur et à mesure de l'avancement du développement — chaque nouvelle règle actée doit y être ajoutée immédiatement.

---

## Transverse / Multi-tenant

- Toute donnée (produit, catégorie, table, commande, article...) est rattachée à un `maquis_id`. Aucune action ne doit permettre à un utilisateur d'agir sur une ressource n'appartenant pas à son maquis courant (`CurrentMaquis::id()`), y compris via un ID deviné dans une requête.
- Un **Super Admin** plateforme (flag global `is_super_admin`, indépendant du système de rôles par maquis) passe outre toute vérification de permission (`Gate::before`).
- Les autorisations métier reposent sur Spatie Permission en mode "teams" (`maquis_id` comme clé d'équipe) : un rôle/une permission est toujours évalué dans le contexte du maquis courant.
- La devise est le Franc CFA (XOF) : tous les montants sont des **entiers** (pas de décimales). Affichage au format "X XXX FCFA".
- Pas de TVA appliquée par défaut (configurable par maquis, à activer ultérieurement).

---

## Produits & Catégories

### Catégories de produits
- Une catégorie appartient à un maquis et possède un `station` (`kitchen`, `bar`, ou `none`) qui détermine vers quel écran de préparation ses produits seront transmis.
- **Une catégorie ne peut pas être supprimée si elle contient encore des produits** (`ProductCategoryService::delete`). Il faut d'abord déplacer ou supprimer les produits associés.
- Le slug d'une catégorie est généré automatiquement à partir de son nom et rendu unique par suffixe numérique (`-2`, `-3`, ...) en cas de collision au sein du maquis.

### Produits
- Un produit doit obligatoirement être rattaché à une catégorie appartenant au **même maquis** (`assertCategoryBelongsToMaquis`) — impossible d'assigner une catégorie d'un autre maquis, y compris lors d'une mise à jour.
- **Un produit utilisé comme composant d'une formule ne peut pas être supprimé** (`ProductService::delete`), afin de ne pas casser les formules existantes.
- Prix promotionnel (`promo_price`) : optionnel, avec fenêtre de validité optionnelle (`promo_starts_at` / `promo_ends_at`).
  - Une promo n'est "active" que si un `promo_price` est défini **et** que l'instant présent est dans la fenêtre de validité (si bornes définies).
  - Le **prix effectif** (`effectivePrice()`) utilisé lors de la prise de commande est le prix promo si actif, sinon le prix normal.
  - Le prix figé sur un article de commande (`unit_price`) est calculé une fois à l'ajout de l'article et ne varie plus ensuite, même si la promo change après coup.
- Le slug d'un produit est généré automatiquement à partir de son nom et rendu unique par suffixe numérique au sein du maquis (en tenant compte des produits supprimés — `withTrashed()` — pour éviter toute collision après restauration).
- Un produit peut être une **formule** (`is_formula`), composée d'autres produits (composants avec quantité) via `product_formula_components`.
- Un produit peut avoir une **recette** (liste d'ingrédients de stock avec quantités) via `product_recipe_items`, utilisée pour la déduction de stock (module Stock, à venir).

---

## Tables

- Une table possède un statut parmi : `free` (libre), `occupied` (occupée), `reserved` (réservée).
- **Réservation** : seule une table libre peut être réservée (`TableService::reserve`).
- **Occupation** : une table déjà occupée ne peut pas être occupée à nouveau (`TableService::occupy`). L'horodatage `occupied_since` est enregistré à l'occupation et effacé à la libération.
- **Libération** : remet la table à `free`, efface `occupied_since` et son éventuel rattachement à un groupe de tables fusionnées.
- **Transfert** : la table de destination doit être libre (`TableService::transfer`). Le statut et l'horodatage d'occupation sont déplacés de la table source vers la table cible ; la table source est ensuite libérée.
- **Fusion (merge)** : toutes les tables secondaires à fusionner avec la table principale doivent être libres (`TableService::merge`) — impossible de fusionner une table déjà occupée ou réservée par ailleurs. Les tables fusionnées héritent du statut et de l'horodatage de la table principale et sont regroupées sous un `TableGroup`.
- **Défusion (unmerge)** : dissocie toutes les tables du groupe et supprime le groupe.
- **Suppression** : une table occupée ou réservée ne peut pas être supprimée (`TableService::delete`) — elle doit d'abord être libérée.

---

## Commandes (Orders)

### Création
- Une commande **sur place** (`dine_in`) doit obligatoirement être associée à une table (`restaurant_table_id` requis) — sinon rejet immédiat (`OrderService::create`).
- Les types de commande possibles : `dine_in` (sur place), `takeaway` (à emporter), `phone` (téléphone / WhatsApp).
- À la création d'une commande sur place, si la table associée est libre, elle est automatiquement passée à `occupied`.
- Chaque article de commande doit référencer un produit **appartenant au même maquis** que la commande — sinon rejet (`OrderService::addItem`).
- Le prix unitaire de chaque article (`unit_price`) est figé au moment de l'ajout, à partir du prix effectif du produit (promo comprise si active à cet instant).
- Le total de la commande (`total`) est recalculé automatiquement à chaque ajout d'article, comme la somme des sous-totaux (`quantité × prix unitaire`) de tous les articles.

### Modification
- **Impossible d'ajouter un article à une commande déjà `completed` (terminée) ou `cancelled` (annulée)** (`OrderService::addItem`).

### Statuts et flux de préparation
- Le flux de statut d'un **article de commande** suit un ordre strict et linéaire : `pending` → `preparing` → `ready` → `served`.
- **Une transition de statut d'article ne peut avancer que d'une seule étape à la fois** dans ce flux (impossible de sauter une étape, ni de reculer) (`OrderService::advanceItemStatus`).
- Le **statut global de la commande** est automatiquement synchronisé sur l'état **le moins avancé parmi tous ses articles** (`syncOrderStatus`) — ex. si un article est `ready` et un autre encore `pending`, la commande reste `pending`. Ce recalcul se fait après chaque avancement de statut d'article.
- Seuls les articles dont le produit appartient à une catégorie de `station` correspondante (`kitchen` ou `bar`) sont visibles sur l'écran Cuisine / Bar respectif (`itemsForStation`), et seulement pour les commandes actives (statut hors `completed`/`cancelled`) et les articles pas encore `served`.

### Annulation
- **Une commande déjà `completed` ne peut pas être annulée** (`OrderService::cancel`).
- Autorisation d'annulation dépendante du statut (`OrderPolicy::cancel`) :
  - Tant que la commande est encore `pending` (pas transmise en cuisine/bar), **tout utilisateur pouvant créer des commandes** peut l'annuler.
  - Une fois la commande transmise (statut ≠ `pending`), **seul un utilisateur disposant de la permission dédiée `orders.cancel`** (ex. Gérant/Admin) peut l'annuler.
- À l'annulation d'une commande sur place, si la table associée n'a **plus aucune autre commande active**, elle est automatiquement libérée.

### Complétion
- **Une commande ne peut être marquée `completed` que si son statut est `served`** (tous ses articles servis) (`OrderService::complete`).
- À la complétion d'une commande sur place, si la table associée n'a **plus aucune autre commande active**, elle est automatiquement libérée.

---

## Cuisine & Bar (stations de préparation)

- Chaque station (Cuisine / Bar) n'affiche que les articles dont le produit appartient à une catégorie configurée sur cette station respective (`kitchen` ou `bar`).
- **Un utilisateur ne peut faire avancer le statut d'un article que pour la station à laquelle il est habilité** :
  - `kitchen.update_status` + l'article doit appartenir à une catégorie `station = kitchen` (`OrderItemPolicy::advanceKitchenStatus`).
  - `bar.update_status` + l'article doit appartenir à une catégorie `station = bar` (`OrderItemPolicy::advanceBarStatus`).
  - Un membre du personnel de cuisine ne peut donc pas faire avancer un ticket bar, et inversement, même s'il a par ailleurs accès à l'écran.
- La progression de statut d'un article suit le même flux strict que décrit ci-dessus (`pending → preparing → ready → served`, une étape à la fois), déclenchée depuis l'écran de la station concernée.
- Le statut global de la commande (visible sur l'écran Commandes) se met à jour automatiquement en fonction de l'avancement combiné des articles cuisine **et** bar d'une même commande.

---

## Caisse (Cashier)

### Session de caisse
- L'encaissement se fait dans le cadre d'une **session de caisse** : un utilisateur (Caissier/Gérant/Admin) ouvre une session avec un **fond de caisse initial** (`opening_amount`), encaisse les commandes pendant son service, puis **clôture** la session en fin de service.
- **Une seule session de caisse peut être ouverte à la fois par maquis** (`CashSessionService::open`) — impossible d'ouvrir une deuxième session tant que la précédente n'est pas clôturée.
- **Impossible d'encaisser un paiement sans session de caisse ouverte** (`PaymentService::collect`) — le bouton d'encaissement est d'ailleurs désactivé côté interface tant qu'aucune session n'est active.
- **Clôture de session** : le montant espèces attendu (`expected_cash_amount`) est calculé automatiquement comme `fond de caisse initial + somme des paiements en espèces encaissés durant la session` (les paiements Mobile Money/carte ne rentrent pas dans le comptage physique). L'utilisateur saisit le montant réellement compté (`closing_amount`), et l'écart (`discrepancy = closing_amount - expected_cash_amount`) est enregistré pour le rapprochement de caisse.
- **Une session déjà clôturée ne peut pas être clôturée à nouveau** (`CashSessionService::close`).

### Paiement des commandes
- Une commande peut être payée en **plusieurs fois avec des moyens différents** (paiement mixte) — ex. moitié en espèces, moitié en Orange Money — tant que la somme des paiements ne dépasse pas le total de la commande.
- Moyens de paiement acceptés : `cash` (espèces), `card` (carte bancaire), `orange_money`, `mtn_momo`, `moov_money`, `wave` (Mobile Money ivoiriens).
- **Un paiement ne peut pas dépasser le solde restant dû** de la commande (`total - somme des paiements déjà encaissés`) — rejeté sinon (`PaymentService::collect`).
- **Impossible d'encaisser un paiement sur une commande annulée** (`cancelled`).
- Le montant d'un paiement doit être strictement positif.
- Chaque paiement encaissé est rattaché à la session de caisse en cours (traçabilité : qui a encaissé, quand, sur quelle session).

### Lien avec le cycle de vie de la commande
- **Une commande ne peut être marquée `completed` que si elle est `served` ET intégralement payée** (`OrderService::complete`, règle renforcée par le module Caisse) — encaissement partiel insuffisant, la commande reste ouverte pour paiement complémentaire.
- Une commande est considérée intégralement payée (`isFullyPaid()`) lorsque le solde restant dû est nul et que son total est strictement positif.

---

## Personnel (Staff) — état actuel

- Un utilisateur (membre du personnel) est rattaché à un maquis avec un rôle donné.
- Les actions de gestion du personnel (`staff.viewAny`, `staff.create`, `staff.update`, `staff.delete`) sont soumises à la permission `staff.manage`.

*(Ce module sera complété avec la règle du badgeage manuel — clock-in/out — lors de son implémentation.)*

---

## Règles à venir (modules non encore implémentés)

Cette section sera complétée au fur et à mesure de l'avancement :
- **Stock** : déduction automatique via les recettes de produits, seuils d'alerte.
- **Achats / Fournisseurs**.
- **Dépenses**.
- **Clients** : fidélité simple — 1 point par montant dépensé.
- **Personnel** : badgeage manuel (clock-in/out).
- **Rapports**.
- **Administration**.
