feat(catalog) : ERP-200 — ProductProvider + ProductProcessor (unicité code, RG-6.03/05/06) #151

Closed
matthieu wants to merge 1 commits from feat/erp-200-product-provider-processor into feat/erp-199-entites-product-storagetype
Owner

Stack sur ERP-199 (base = feat/erp-199-entites-product-storagetype).

ERP-200 (1.4) — Logique métier du produit (M6)

ProductProvider (lecture)

  • Liste paginée Hydra (Paginator ORM, règle ABSOLUE n°13) — jamais d'array brut.
  • Exclut les produits soft-deleted par défaut (RG-6.09) ; tri name ASC.
  • Filtres drawer : ?search (code+name, LIKE échappé), ?categoryId/?categoryCode, ?state (containment JSONB @>), ?siteId[] (EXISTS corrélé).
  • Get item : 404 si soft-deleted (non exposé au M6, § 2.7).
  • Échappatoire ?pagination=false respectée.

ProductProcessor (écriture POST/PATCH)

  1. RG-6.07 — normalisation serveur code trim+UPPER, name trim (ProductFieldNormalizer, miroir CarrierFieldNormalizer).
  2. RG-6.03manufactured/containsMolasses forcés false si states ne contient pas SALE.
  3. RG-6.01 — unicité globale du code parmi les actifs → 409 (pré-check excluant le produit courant en PATCH + filet anti-race sur l'index partiel). PropertyPath code côté front.
  4. Mode strict PATCH : un seul niveau catalog.products.manage (admin-only § 5.2) → 403 global porté par la security d'opération, aucun guard de champ nécessaire.

Entité Product

  • Assert\Callback RG-6.05 (catégorie de type PRODUIT) + RG-6.06 (types de stockage disponibles sur au moins un site choisi), ->atPath() pour mapping inline 422 (useFormErrors / ERP-101).
  • Constantes d'états (PURCHASE/SALE/OTHER).

Repository

  • createListQueryBuilder (filtres + eager-load category/sites/storageTypes).

Vérifications

  • make test 873 tests, 6541 assertions (dont CollectionsArePaginatedTest, EntityConstraintsHaveFrenchMessageTest).
  • make php-cs-fixer-allow-risky
  • Tests fonctionnels RG-6.01→6.10 : ticket ERP-203.
Stack sur ERP-199 (base = `feat/erp-199-entites-product-storagetype`). ## ERP-200 (1.4) — Logique métier du produit (M6) ### ProductProvider (lecture) - Liste **paginée Hydra** (Paginator ORM, règle ABSOLUE n°13) — jamais d'array brut. - Exclut les produits **soft-deleted** par défaut (RG-6.09) ; tri `name ASC`. - Filtres drawer : `?search` (code+name, LIKE échappé), `?categoryId`/`?categoryCode`, `?state` (containment JSONB `@>`), `?siteId[]` (EXISTS corrélé). - Get item : **404** si soft-deleted (non exposé au M6, § 2.7). - Échappatoire `?pagination=false` respectée. ### ProductProcessor (écriture POST/PATCH) 1. **RG-6.07** — normalisation serveur `code` trim+UPPER, `name` trim (ProductFieldNormalizer, miroir CarrierFieldNormalizer). 2. **RG-6.03** — `manufactured`/`containsMolasses` forcés `false` si `states` ne contient pas `SALE`. 3. **RG-6.01** — unicité **globale** du code parmi les actifs → **409** (pré-check excluant le produit courant en PATCH + filet anti-race sur l'index partiel). PropertyPath `code` côté front. 4. **Mode strict PATCH** : un seul niveau `catalog.products.manage` (admin-only § 5.2) → 403 global porté par la security d'opération, aucun guard de champ nécessaire. ### Entité Product - `Assert\Callback` **RG-6.05** (catégorie de type PRODUIT) + **RG-6.06** (types de stockage disponibles sur au moins un site choisi), `->atPath()` pour mapping inline 422 (useFormErrors / ERP-101). - Constantes d'états (PURCHASE/SALE/OTHER). ### Repository - `createListQueryBuilder` (filtres + eager-load category/sites/storageTypes). ### Vérifications - `make test` ✅ **873 tests, 6541 assertions** (dont `CollectionsArePaginatedTest`, `EntityConstraintsHaveFrenchMessageTest`). - `make php-cs-fixer-allow-risky` ✅ - Tests fonctionnels RG-6.01→6.10 : ticket **ERP-203**.
matthieu added 1 commit 2026-06-25 09:16:28 +00:00
Provider de lecture (liste paginée Hydra filtrée + item) :
- exclut les produits soft-deleted (RG-6.09), tri name ASC ;
- filtres ?search (code+name), ?categoryId/?categoryCode, ?state (JSONB @>), ?siteId[] (EXISTS) ;
- Get item : 404 sur soft-deleted (non exposé au M6, § 2.7) ;
- pagination obligatoire via Paginator ORM (règle n°13), échappatoire ?pagination=false.

Processor d'écriture (POST/PATCH) :
- normalisation serveur code trim+UPPER, name trim (RG-6.07, ProductFieldNormalizer) ;
- RG-6.03 : manufactured/containsMolasses forcés false si states sans SALE ;
- RG-6.01 : unicité globale du code parmi les actifs -> 409 (pré-check + filet anti-race index partiel), propertyPath code côté front.

Entité Product : Assert\Callback RG-6.05 (catégorie de type PRODUIT) et RG-6.06
(types de stockage disponibles sur au moins un site choisi), atPath pour mapping
inline 422 ; constantes d'états.

Repository : createListQueryBuilder (filtres + eager-load category/sites/storageTypes)
+ existsActiveByCode déjà en place.

make test vert (873 tests), php-cs-fixer OK.
matthieu added the backM6-Produittype/feat labels 2026-06-25 09:17:00 +00:00
Author
Owner

Review back — ERP-200 (ProductProvider + ProductProcessor), read-only.

Périmètre : provider liste/détail, processor création/édition, RG-6.03/05/06, normalisation.

Vérifié conforme :

  • Pagination (règle ABSOLUE n°13) : ProductProvider enveloppe ApiPlatform\…\Paginator (fetchJoinCollection: true), ?pagination=false géré via pagination->isEnabled() — aucun array brut. Garde-fou CollectionsArePaginatedTest vert.
  • Anti-N+1 : liste fetch-join category + sites + storageTypes.
  • RG-6.01 : unicité code parmi les actifs → 409, exclusion du produit courant en PATCH + filet anti-race via l'index partiel. Le 409 (ConflictHttpException) est mappé front sur code (même contrat que CategoryProcessor/RG-1.07).
  • RG-6.07 : normalisation code trim+UPPER / name trim appliquée avant la vérification d'unicité.
  • RG-6.03/05/06 : cohérence inter-champs en #[Assert\Callback] avec ->atPath() + messages FR (consommable par useFormErrors).

Verdict : aucun retour obligatoire.

**Review back — ERP-200 (ProductProvider + ProductProcessor), read-only.** Périmètre : provider liste/détail, processor création/édition, RG-6.03/05/06, normalisation. Vérifié conforme : - Pagination (règle ABSOLUE n°13) : `ProductProvider` enveloppe `ApiPlatform\…\Paginator` (`fetchJoinCollection: true`), `?pagination=false` géré via `pagination->isEnabled()` — aucun array brut. Garde-fou `CollectionsArePaginatedTest` vert. - Anti-N+1 : liste fetch-join `category` + `sites` + `storageTypes`. - RG-6.01 : unicité `code` parmi les actifs → 409, **exclusion du produit courant en PATCH** + filet anti-race via l'index partiel. Le 409 (`ConflictHttpException`) est mappé front sur `code` (même contrat que `CategoryProcessor`/RG-1.07). - RG-6.07 : normalisation `code` trim+UPPER / `name` trim appliquée **avant** la vérification d'unicité. - RG-6.03/05/06 : cohérence inter-champs en `#[Assert\Callback]` avec `->atPath()` + messages FR (consommable par `useFormErrors`). Verdict : ✅ aucun retour obligatoire.
Author
Owner

Review M6 « Produit » — relecture croisée (couche données + couche application) sur le diff cumulé develop…HEAD.

Verdict : 0 bloquant, 0 important.

  • Garde-fous archi verts : COMMENT ON COLUMN, Timestampable/Blamable, Auditable + i18n, pagination (règle absolue n°13), contraintes Assert\* en FR.
  • RBAC 3 miroirs alignés (sidebar / personas / SeedE2ECommand).
  • Export : priority:1 + #[IsGranted('catalog.products.view')], mêmes filtres que la liste — conforme.
  • 119 tests Catalog + garde-fous archi verts en isolation (BDD fraîche).

Nits relevés (non bloquants) :

  • product.states : DEFAULT '[]'::jsonb contredisait le CHECK (jsonb_array_length(states) >= 1) (default mort, jamais atteignable) → corrigé (commit 30e7839).
  • ProductExportControllerTest : le tearDown réutilisait la constante des category-types (TEST_CATEGORY_TYPE_PREFIX) pour purger des storage-types → constante dédiée TEST_STORAGE_PREFIX (commit 30e7839).
  • Import Site (cross-module) du controller utilisé seulement dans un commentaire @var ; DQL p.id != :id au lieu de <> : cosmétiques, laissés tels quels.
  • Provider ?pagination=false renvoyant un array : conforme (pattern établi ClientProvider/SupplierProvider, échappatoire documentée pour alimenter les selects).
**Review M6 « Produit »** — relecture croisée (couche données + couche application) sur le diff cumulé `develop…HEAD`. **Verdict : 0 bloquant, 0 important.** - Garde-fous archi verts : COMMENT ON COLUMN, Timestampable/Blamable, Auditable + i18n, pagination (règle absolue n°13), contraintes `Assert\*` en FR. - RBAC 3 miroirs alignés (sidebar / personas / SeedE2ECommand). - Export : `priority:1` + `#[IsGranted('catalog.products.view')]`, mêmes filtres que la liste — conforme. - 119 tests Catalog + garde-fous archi verts en isolation (BDD fraîche). **Nits relevés (non bloquants) :** - `product.states` : `DEFAULT '[]'::jsonb` contredisait le `CHECK (jsonb_array_length(states) >= 1)` (default mort, jamais atteignable) → corrigé (commit `30e7839`). - `ProductExportControllerTest` : le `tearDown` réutilisait la constante des category-types (`TEST_CATEGORY_TYPE_PREFIX`) pour purger des storage-types → constante dédiée `TEST_STORAGE_PREFIX` (commit `30e7839`). - Import `Site` (cross-module) du controller utilisé seulement dans un commentaire `@var` ; DQL `p.id != :id` au lieu de `<>` : cosmétiques, laissés tels quels. - Provider `?pagination=false` renvoyant un `array` : conforme (pattern établi `ClientProvider`/`SupplierProvider`, échappatoire documentée pour alimenter les selects).
Author
Owner

Consolidée dans #154 : la pile M6 a été reciblée sur develop en une seule MR pour un CI unique. Commits intégralement inclus dans #154 — fermée sans merge individuel.

Consolidée dans #154 : la pile M6 a été reciblée sur `develop` en une seule MR pour un CI unique. Commits intégralement inclus dans #154 — fermée sans merge individuel.
matthieu closed this pull request 2026-06-25 12:36:07 +00:00

Pull request closed

Sign in to join this conversation.