# Module — Stock & inventaire

> Quantités par produit/dépôt (`StockItem`), mouvements (`StockMovement` : entrée/sortie/ajustement/vente), inventaires, seuils d'alerte (stock bas → notification), multi-dépôt optionnel (`Warehouse`, ADR-OPEN-E). Mouvements **append-only** (ADR-006). Fiche conforme au [canevas des modules](README.md).

## 0. État d'implémentation — incrément 1 (2026-07-24) 🚧

Suivi de stock **livré et testé** : niveaux par produit, sortie automatique à la vente,
mouvements manuels, stock affiché dans le catalogue. Écarts assumés vs la cible :

| Livré ✅ | Cible non encore faite ⬜ |
|---|---|
| `StockItem` (un niveau `quantityOnHand` par produit, mono-dépôt) | multi-dépôt (`Warehouse`), stock par variante |
| `StockMovement` append-only (`in`/`out`/`sale`/`adjustment`, `quantityDelta` signé, `resultingQuantity`, lien vente) | push mobile, digest quotidien |
| **Seuil d'alerte stock bas** : `Product.lowStockThreshold`, `GET /api/v1/stock/alerts` (produits actifs sous le seuil), `lowStock`/`lowStockThreshold` dans la fiche produit ; PWA : champ seuil, puce « Stock bas », écran Alertes + badge | seuil par catégorie, réappro auto suggéré |
| **Notification proactive** au franchissement descendant du seuil (vente/mouvement/inventaire) : `LowStockAlertService` → notification in-app au(x) manager(s) + e-mail (fail-soft, dédupliqué via `AlertDeduplicationService`) | canal push, préférences de notification par utilisateur |
| `StockService` : sortie `sale` générée automatiquement à l'encaissement (`SaleController`) | réservation, gestion des retours/avoirs → entrée |
| **Inventaire physique** : `InventoryCount`/`InventoryCountLine` (`INV-AAAA-000000`, `draft`→`applied`/`cancelled`), `InventoryService` (application = ajustements recalés sur le stock réel au moment de l'application) | multi-dépôt, inventaire par zone, comptage aveugle multi-opérateurs |
| `GET /api/v1/stock` (niveaux paginés), `POST /api/v1/stock/movements` (in/out/adjustment) ; `/api/v1/inventory-counts` (créer/lister/détail, `PUT /lines`, `POST /apply` idempotent, `POST /cancel`) | mouvements paginés/filtrés, import CSV |
| `stockQuantity` exposé dans la fiche produit (`GET /products`, lookup batch anti N+1) | seuil `lowStock`, disponibilité e-commerce |
| Gating : lecture membre (`COMPANY_VIEW`), mouvements manuels + inventaire manager (`COMPANY_EDIT`) | permission dédiée `stock.*` (bascule RBAC phase 2) |
| Écran PWA : **stock affiché** dans les listes Produits et Vente (rouge si ≤ 0) ; écran **Inventaire** (liste + comptage avec écart live + application) accès Profil→Gestion | écran d'ajustement/réappro dédié, alertes |
| Tests `tests/Functional/Api/StockApiTest.php` (6) + `InventoryApiTest.php` (7 : recalage, idempotence, non-impact non comptées, annulation) | tests seuils |

**Note** : mono-dépôt ; la survente est autorisée (stock peut devenir négatif). Fixtures : stock
initial par produit (`AppFixtures::createDemoCatalogue`). L'inventaire ignore les lignes non
comptées (aucun impact) et est **idempotent** (une seconde application ne rebouge pas le stock).
Fichiers : `src/Entity/{StockItem,StockMovement,InventoryCount,InventoryCountLine}.php`,
`src/Service/Pos/{StockService,InventoryService}.php`,
`src/Controller/Api/V1/{StockController,InventoryCountController}.php` ; PWA
`yima-shop/src/app/features/inventory/`, `core/services/inventory.service.ts`.

La suite de la fiche décrit la **cible complète**.

## 1. Objectif

Suivre la disponibilité réelle des produits d'une boutique, tracer chaque variation par un mouvement auditable, alerter sur les ruptures, et exposer la disponibilité au site e-commerce.

- **Manager (portail web)** : réceptionne du stock (entrée), ajuste, lance des inventaires, définit les seuils d'alerte, consulte l'historique (`stock.view`, `stock.movement.create`, `stock.inventory.manage`).
- **Employé / caissier (PWA)** : la vente génère automatiquement des sorties ; consultation de disponibilité (`stock.view`).

## 2. Périmètre

**In (v1)**
- `StockItem` : quantité courante par `(Product/Variant, Warehouse)`.
- `StockMovement` : `in` (réception), `out` (sortie manuelle), `adjustment` (correction), `sale` (généré par vente), `return` (avoir).
- Inventaire : comptage → écarts → mouvements `adjustment`.
- Seuil d'alerte par produit (`lowStockThreshold`) → notification stock bas.
- **Multi-dépôt optionnel** via `Warehouse` (un dépôt par défaut par boutique si non activé).

**Out (v1) → évolutions**
- Réappro automatique / commandes fournisseur (module Achats, phase 3).
- Valorisation du stock (FIFO/CMUP) et coûts — évolution.
- Transferts inter-boutiques.
- Lots / DLC / numéros de série.

## 3. Entités

Rattachées à `Company`, `TimestampableEntity`, `uuid` public. `StockMovement` **append-only, sans soft-delete** (ADR-006) ; `StockItem` est un solde mis à jour.

| Entité | Champs clés | Relations |
|---|---|---|
| `Warehouse` | `uuid`, `name`, `isDefault`, `isActive` | `Company (1)`, `StockItem (N)` |
| `StockItem` | `uuid`, `quantity` (entier), `lowStockThreshold`, `reservedQuantity` | `Company (1)`, `Product (1)`, `ProductVariant (0..1)`, `Warehouse (1)` |
| `StockMovement` | `uuid`, `type` (`in`/`out`/`adjustment`/`sale`/`return`), `quantity` (signé), `reason`, `occurredAt`, `sourceRef` (ex. `Sale.uuid`) | `Company (1)`, `Product (1)`, `ProductVariant (0..1)`, `Warehouse (1)`, `User` auteur (1) |

- `Warehouse (1) ── (N) StockItem` ; `StockItem` unique par `(Product, Variant, Warehouse)`.
- `StockMovement (N) ── (1) Product` ; chaque mouvement ajuste le `StockItem` correspondant.
- Une **vente confirmée** génère des `StockMovement` `sale` (sortie) ; un **avoir** génère des `return` (entrée) — voir [POS](pos-ventes.md).

## 4. Permissions

| Code | Action | Rôles v1 |
|---|---|---|
| `stock.view` *(nouveau)* | Consulter stock, mouvements, alertes | Employé, Manager |
| `stock.movement.create` *(nouveau)* | Saisir entrée/sortie/ajustement manuel | Manager |
| `stock.inventory.manage` *(nouveau)* | Lancer/valider un inventaire | Manager |
| `stock.warehouse.manage` *(nouveau)* | Gérer les dépôts (si multi-dépôt) | Manager |

Gating : `denyAccessUnlessGranted('stock.movement.create', $company)` ; `{% if is_granted('stock.view', company) %}`. Les mouvements `sale`/`return` sont créés **par le système** (flux POS), pas via `stock.movement.create`.

## 5. API

Base `/api/v1`, Bearer, enveloppe standard, scope boutique imposé serveur. Créations : `Idempotency-Key`.

| Méthode | Route | Corps attendu | Permission |
|---|---|---|---|
| GET | `/api/v1/stock` | — (`?filter[warehouse]=&filter[low]=1&q=`, pagination) | `stock.view` |
| GET | `/api/v1/stock/{productUuid}` | — (par dépôt) | `stock.view` |
| POST | `/api/v1/stock/movements` | `{ uuid, productUuid, variantUuid?, warehouseUuid?, type, quantity, reason }` | `stock.movement.create` |
| POST | `/api/v1/inventories` | `{ uuid, warehouseUuid?, lines:[{productUuid,variantUuid?,countedQuantity}] }` | `stock.inventory.manage` |
| GET | `/api/v1/stock/alerts` | — (produits sous seuil) | `stock.view` |

- 422 si `type=out/adjustment` mène à une quantité négative interdite (selon politique) ou `quantity=0` ; 404 hors boutique.

## 6. Écrans

**Web (Twig — Manager)**
- Tableau de stock : produit, dépôt, quantité, seuil, **badge stock bas**, filtres.
- Saisie de mouvement : type, produit/variante, dépôt, quantité, motif.
- Inventaire : sélection dépôt → grille de comptage → aperçu des écarts → validation (génère les `adjustment`).
- Écran alertes / ruptures.

**PWA (caissier)**
- Indicateur de disponibilité sur la fiche produit lors de l'encaissement (lecture).
- Sortie stock automatique et transparente à la confirmation de vente (hors-ligne : rejeu idempotent).

## 7. Règles métier

- **Solde** : `StockItem.quantity += Σ StockMovement.quantity` (signé) ; recalcul cohérent avec l'historique (audit possible).
- **Signes** : `in`/`return` positifs ; `out`/`sale` négatifs ; `adjustment` signé selon l'écart.
- **Append-only** : un mouvement erroné se corrige par un **mouvement inverse**, jamais par suppression/édition (ADR-006).
- **Vente** : à la confirmation d'une `Sale`, un `sale` par ligne (`sourceRef = Sale.uuid`) ; l'avoir crée les `return` symétriques.
- **Inventaire** : `adjustment.quantity = countedQuantity - quantity` par ligne ; l'inventaire est atomique (tout ou rien).
- **Seuil** : `quantity <= lowStockThreshold` → événement stock bas → notification (dédup via `AlertDeduplicationService`, jamais d'alerte directe).
- **Multi-dépôt** : si désactivé, tout passe par le `Warehouse` `isDefault` de la boutique ; `warehouseUuid` optionnel dans l'API (ADR-OPEN-E).
- **Idempotence** : `(company, movement.uuid)` unique — rejeu POS hors-ligne sans double décrément.
- Quantités **entières** ; scope `Company` sur toute requête ; fuseau `Africa/Dakar` pour `occurredAt`.

## 8. Projection e-commerce

- **Répliqué (lecture)** : la **disponibilité** (`quantity - reservedQuantity`) agrégée par produit publiable → base client, pour l'affichage « en stock / rupture ».
- **Non répliqué** : mouvements, motifs, coûts, détail par dépôt (surface réduite).
- Upsert **idempotent** par clé métier (`company + product`) ; **le central gagne** (ADR-001) ; le site ne fait que lire — une commande site remonte au centre et y décrémente (flux entrant, hors v1). Périmètre exact = ADR-OPEN-B.

## 9. Tests

**Unitaires**
- Application des signes par type ; recalcul du solde depuis l'historique.
- Écart d'inventaire (`counted - current`) → `adjustment` correct.
- Déclenchement seuil stock bas (au passage sous le seuil, une fois).

**Fonctionnels**
- `POST /stock/movements` (in/out/adjustment) → solde mis à jour.
- Confirmation de vente → mouvements `sale` (`sourceRef`) ; avoir → `return`.
- Rejeu `Idempotency-Key` → pas de double mouvement.
- Inventaire multi-lignes → écarts appliqués atomiquement.

**Accès**
- Employé sans `stock.movement.create` → 403 sur saisie manuelle.
- Manager boutique A ajustant du stock boutique B → 403/404.

## 10. Dépendances

- [Catalogue produits](catalogue-produits.md) — produits/variantes référencés.
- [POS / Ventes](pos-ventes.md) — génère mouvements `sale`/`return`.
- [Notifications](README.md) — alertes stock bas.
- Socle : RBAC/`PermissionVoter`, `EntityManagerService`, `FileLogger`, `ApiResponseFormatter`, `AlertDeduplicationService`, JWT (firewall `api`).

## 11. Notes / ADR liés

- Hub : [../README.md](../README.md) · Modèle : [../02-modele-de-donnees.md](../02-modele-de-donnees.md) · Sécurité : [../03-securite.md](../03-securite.md) · API : [../04-api.md](../04-api.md) · Catalogue : [README.md](README.md).
- ADR : [../notes/decisions.md](../notes/decisions.md) — **ADR-006** (mouvements append-only), **ADR-002** (RBAC), **ADR-001** (central autoritaire / projection disponibilité), **ADR-OPEN-E** (multi-dépôt v1 ou plus tard), **ADR-OPEN-B** (périmètre projection).
