# Module — Catalogue produits

> Produits, variantes, catégories arborescentes par boutique, prix, code-barres/SKU, TVA par produit, activation, images (via `Media` existant). **Soft-delete autorisé ici** (produits/catégories). Base publiable vers l'e-commerce. Fiche conforme au [canevas des modules](README.md).

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

Première tranche verticale **livrée et testée** (API + écran PWA Produits). Écarts assumés
vs la cible ci-dessous, à combler dans les incréments suivants :

| Livré ✅ | Cible non encore faite ⬜ |
|---|---|
| `Product` : `uuid`, `name`, `slug`, `sku`, `barcode`, **`priceCents`** (centimes XOF), **`vatRate`** (%, ex. 18), `isActive`, catégorie, soft-delete, timestamps | `ProductVariant`, `isPublishable`, image (`Media`), `description` produit |
| `ProductCategory` : `uuid`, `name`, `slug`, `description`, `parent?`, `isActive`, soft-delete | `position`, `isPublishable`, arbre drag-and-drop |
| API `GET /api/v1/products` (pagination `?page=&limit=`, `?search=`, `?category=`, `?active=`), `GET /api/v1/products/{uuid}`, `POST/PUT /api/v1/products` | `DELETE` (soft-delete), variantes dans le payload |
| API `GET /api/v1/categories`, `POST /api/v1/categories` | route nommée `/product-categories`, déplacement/arbre |
| Permissions **`catalogue.view` / `catalogue.manage`** (seedées, `RbacSeedCommand`) | permissions granulaires `catalogue.product.*` / `catalogue.category.manage` |
| Gating via `CompanyVoter` (`COMPANY_VIEW` lecture, `COMPANY_EDIT` écriture) + scope `getPrimaryCompany()` | bascule autorité `boutiqueRole` (phase 2 RBAC) |
| Écran **PWA Produits** : liste paginée (infinite scroll), recherche, pull-to-refresh, prix FCFA, chips catégorie, **stock affiché** | scan code-barres, détail produit, cache offline (service worker) |
| **Gestion produits PWA** : création (FAB) et édition (au tap) via formulaire modal (nom, prix, TVA, SKU, code-barres, catégorie, actif) → `POST/PUT /products` | gestion des catégories/variantes en UI |
| **Images produit** (F2, 2026-07-25) : `ProductImage` (réutilise `Media`), API `/api/v1/products/{uuid}/images` (upload/liste/service binaire/principale/suppression) ; `primaryImageUuid` dans la fiche ; PWA : vignettes liste + section **Photos** au formulaire (directive `appAuthImg`) | variantes, recadrage/vignettes serveur |
| Tests `tests/Functional/Api/CatalogueApiTest.php` (5) + `ProductImageApiTest.php` (3) | tests unitaires (unicité SKU, arbre catégories) |

**Notes d'écart** : le prix est stocké en `priceCents` (et non `basePrice`) et la TVA en
**pourcentage entier** `vatRate` (et non en points de base `taxRate`). Fixtures de démo :
`AppFixtures::createDemoCatalogue` (3 catégories, 7 produits pour AmansaShop).
Fichiers : `src/Entity/Product.php`, `src/Entity/ProductCategory.php`,
`src/Controller/Api/V1/{ProductController,CategoryController}.php`,
`yima-shop/src/app/features/catalogue/`, `yima-shop/src/app/core/services/catalogue.service.ts`.

La suite de la fiche décrit la **cible complète** (variantes, publiable, permissions granulaires).

## 1. Objectif

Donner à chaque boutique un catalogue structuré, recherchable au comptoir (scan code-barres) et publiable vers un site e-commerce.

- **Manager (portail web)** : crée/édite/organise produits, variantes et catégories, fixe prix et TVA, active/désactive, marque « publiable » (`catalogue.product.*`).
- **Employé / caissier (PWA)** : consulte et recherche le catalogue (lecture, `catalogue.product.view`) pour l'encaissement — pas d'édition en v1.

## 2. Périmètre

**In (v1)**
- `Product` : nom, description, `sku`, `barcode`, `basePrice` (centimes XOF), `taxRate` (bps), `isActive`, `isPublishable`, catégorie, image (`Media`).
- `ProductVariant` : déclinaisons (taille/couleur…), `sku`/`barcode` propres, surcharge de prix optionnelle.
- `ProductCategory` : arborescence **par boutique** (parent/enfant).
- Recherche par nom/SKU/code-barres ; activation/désactivation.
- **Soft-delete** (Gedmo) sur produits et catégories.

**Out (v1) → évolutions**
- Prix par canal / liste de prix multiples (v1 = un prix + surcharge variante).
- Attributs dynamiques / fiches produit riches e-commerce (SEO, galeries).
- Gestion des lots / numéros de série (relève du stock avancé).
- Import/export de masse (CSV) — évolution.
- Promotions et règles de prix (module Promotions, phase 3).

## 3. Entités

Rattachées à `Company`, `TimestampableEntity`, `uuid` public. **Soft-delete Gedmo + index `idx_<table>_deleted`** (entités effaçables).

| Entité | Champs clés | Relations |
|---|---|---|
| `Product` | `uuid`, `name`, `description`, `sku` (unique/boutique), `barcode`, `basePrice`, `taxRate` (bps), `isActive`, `isPublishable`, `deletedAt` | `Company (1)`, `ProductCategory (0..1)`, `ProductVariant (N)`, `Media` image (0..1) |
| `ProductVariant` | `uuid`, `label`, `sku`, `barcode`, `priceOverride?`, `isActive`, `deletedAt` | `Product (N)`, `Company (1)` |
| `ProductCategory` | `uuid`, `name`, `slug`, `parent?`, `position`, `isPublishable`, `deletedAt` | `Company (1)`, `ProductCategory` parent (0..1), enfants (N), `Product (N)` |

- `Product (1) ── (N) ProductVariant` ; `ProductCategory (1) ── (N) Product`.
- `ProductCategory` auto-référencée (arborescence, cycle interdit), **scopée boutique**.
- Image via le système `Media` existant (company-scoped) — pas de stockage binaire propre.

## 4. Permissions

| Code | Action | Rôles v1 |
|---|---|---|
| `catalogue.product.view` *(nouveau)* | Lister/consulter produits & catégories | Employé, Manager |
| `catalogue.product.create` *(nouveau)* | Créer produit/variante | Manager |
| `catalogue.product.edit` *(nouveau)* | Modifier, activer/désactiver, publier | Manager |
| `catalogue.product.delete` *(nouveau)* | Soft-delete produit/variante | Manager |
| `catalogue.category.manage` *(nouveau)* | Créer/éditer/déplacer/supprimer catégories | Manager |

Seeding via `RbacSeedCommand` ; gating `denyAccessUnlessGranted('catalogue.product.edit', $company)` et `{% if is_granted('catalogue.product.create', company) %}`.

## 5. API

Base `/api/v1`, Bearer, enveloppe standard, scope boutique imposé serveur.

| Méthode | Route | Corps attendu | Permission |
|---|---|---|---|
| GET | `/api/v1/products` | — (`?filter[active]=&filter[category]=&q=`, pagination) | `catalogue.product.view` |
| GET | `/api/v1/products/{uuid}` | — | `catalogue.product.view` |
| POST | `/api/v1/products` | `{ name, sku, barcode?, basePrice, taxRate, categoryUuid?, isActive?, isPublishable?, variants?:[…] }` | `catalogue.product.create` |
| PUT | `/api/v1/products/{uuid}` | champs modifiables | `catalogue.product.edit` |
| DELETE | `/api/v1/products/{uuid}` | — (soft-delete) | `catalogue.product.delete` |
| GET | `/api/v1/product-categories` | — (arbre) | `catalogue.product.view` |
| POST | `/api/v1/product-categories` | `{ name, parentUuid?, position?, isPublishable? }` | `catalogue.category.manage` |

- 422 si `sku` en doublon dans la boutique ou `basePrice < 0` ; 404 hors boutique.

## 6. Écrans

**Web (Twig — Manager)**
- Liste produits : recherche, filtres (catégorie, actif/inactif, publiable), badges, prix XOF.
- Formulaire produit : infos, prix, TVA, catégorie, variantes (répéteur), upload image (`Media`), interrupteurs `isActive`/`isPublishable`.
- Gestion des catégories : arbre drag-and-drop, création/renommage/déplacement.

**PWA (caissier)**
- Recherche produit rapide (nom/SKU/**scan code-barres**), affichage prix/TVA, sélection variante — **lecture seule**.
- Catalogue mis en cache (service worker) pour usage hors-ligne.

## 7. Règles métier

- **Prix effectif** : `variant.priceOverride ?? product.basePrice` ; toujours en **centimes XOF**, jamais négatif.
- **TVA** : `taxRate` en points de base (ex. 1800 = 18 %) au niveau produit ; hérité par les lignes de vente.
- **Unicité** : `sku` unique **par boutique** (variantes incluses) ; `barcode` unique par boutique si renseigné.
- **Catégorie** : parent dans la même boutique ; cycle interdit ; suppression d'une catégorie non-vide → détacher/déplacer produits (pas d'orphelin).
- **Activation** : un produit `isActive = false` reste vendable au retour/avoir mais masqué de la recherche caissier.
- **Publiable** : `isPublishable = true` = éligible à la projection e-commerce.
- **Soft-delete** : produit supprimé conservé (Gedmo) ; les ventes passées gardent leur libellé/SKU **dénormalisés** (voir [POS](pos-ventes.md)) — pas de casse historique.
- Toute requête filtrée par la `Company` de l'utilisateur.

## 8. Projection e-commerce

- **Répliqué (lecture)** vers la base client : produits `isPublishable && isActive && !deletedAt`, leurs variantes, catégories `isPublishable`, prix et TVA.
- **Non répliqué** : produits internes non publiables, coûts d'achat, marges.
- Upsert **idempotent** par clé métier (`company + sku`) ; **le central gagne** (ADR-001) ; le site n'écrit jamais le catalogue.
- Disponibilité (stock) fournie séparément par le [module Stock](stock-inventaire.md). Périmètre exact = ADR-OPEN-B.

## 9. Tests

**Unitaires**
- Prix effectif (avec/sans `priceOverride`).
- Validation unicité SKU/barcode par boutique.
- Intégrité arbre catégories (rejet de cycle, parent hors boutique).

**Fonctionnels**
- CRUD produit complet ; `DELETE` → soft-delete (absent des listes, présent en base).
- Création SKU dupliqué → 422.
- Arbre catégories via API.

**Accès**
- Employé (lecture seule) tentant `POST /products` → 403.
- Manager boutique A éditant un produit boutique B → 403/404.

## 10. Dépendances

- Socle `Media` (images produit), RBAC/`PermissionVoter`, `EntityManagerService`, `FileLogger`, `ApiResponseFormatter`, Gedmo soft-delete.
- Consommé par : [POS / Ventes](pos-ventes.md), [Stock & inventaire](stock-inventaire.md), [Projection e-commerce](README.md).

## 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-004** (centimes XOF), **ADR-002** (RBAC), **ADR-001** (central autoritaire / projection), **ADR-OPEN-B** (périmètre projection e-commerce).
