feat(catalog) : M7 — StorageProvider + StorageProcessor (ERP-213) #165

Merged
tristan merged 1 commits from feat/erp-213-storage-provider-processor into develop 2026-06-30 06:00:01 +00:00
Owner

M7 · ERP-213 (1.4) — StorageProvider + StorageProcessor

⚠️ MR empilée sur feat/erp-212-entite-storage (#164) → #163#162. À merger dans l'ordre 210 → 211 → 212 → 213.

Expose les opérations API stockage (liste paginée + filtres, création/édition) avec normalisation serveur et règles métier. Pattern miroir ProductProvider/ProductProcessor.

Livré

  • StorageProvider (ORM) — collection paginée Hydra (ApiPlatform\Doctrine\Orm\Paginator, jamais d'array brut), échappatoire ?pagination=false. Exclut les soft-deleted (RG-7.07). Tri site.code ASC, storageType.label ASC, numero ASC. Filtres ?search= (numero), ?siteId[]=, ?storageTypeId=, ?state=. Item : lookup par id, 404 si soft-deleted (§ 2.8).
  • StorageProcessor (Post/Patch) — StorageFieldNormalizer (numero → trim, pas d'UPPER, RG-7.06 / HP-M7-05) ; RG-7.01 unicité (site, storageType, numero) parmi les actifs → 409 (exclut le courant en PATCH, filet anti-race sur l'index partiel) ; mode strict PATCH.
  • Repository étendu : existsActiveBySiteTypeNumero() + createListQueryBuilder() (jointures to-one site/storageType eager-load, filtres, JSONB @> pour ?state=).
  • Tests : AbstractStorageApiTestCase + StorageApiTest (10 cas).

Décision — RG-7.03 omise (validée avec Tristan)

Le prompt demandait de vérifier que storageType.sites contient le site. Mais ce concept a été retiré du modèle en M6 : la jointure storage_type_site a été droppée (migration Version20260626100000) et StorageType rendu plat — l'entité le documente explicitement (« un type n'est PAS rattaché à des sites ; la dispo relève de la future entité Stockage »). C'est désormais Storage (1 site + 1 type) qui matérialise cette disponibilité : il n'existe plus de référentiel à interroger. RG-7.03 est donc inimplémentable telle quelle et omise. À reclarifier côté spec. (De même, le filtre ?siteId[]= du StorageTypeProvider cité par le prompt n'existe pas — supprimé en M6 ; le filtre site est porté ici directement par Storage.site.)

Vérifications

  • StorageApiTest (10 tests, 64 assertions) : collection paginée Hydra + contrat de sérialisation (site/storageType objets embarqués, displayName présent) ; 201 création ; trim numéro ; 409 doublon triplet ; même numéro sur autre type → 201 ; réutilisation après soft-delete → 201 ; soft-deleted → 404 ; view lit / ne gère pas ; 403 des 4 personas métier (admin-only).
  • make test : CollectionsArePaginatedTest vert. Les échecs de la suite complète (Core/Commercial/Sites/Transport) sont du flaky JWT + pollution BDD (suite sans DAMA) — chacun passe sur BDD propre / en isolation ; les 4 rejoués ensemble avec StorageApiTest sur BDD propre = 38/38.
  • make php-cs-fixer-allow-risky : clean.
## M7 · ERP-213 (1.4) — `StorageProvider` + `StorageProcessor` > ⚠️ **MR empilée** sur `feat/erp-212-entite-storage` (#164) → #163 → #162. À merger dans l'ordre 210 → 211 → 212 → 213. Expose les opérations API stockage (liste paginée + filtres, création/édition) avec normalisation serveur et règles métier. Pattern miroir `ProductProvider`/`ProductProcessor`. ### Livré - **`StorageProvider`** (ORM) — collection **paginée Hydra** (`ApiPlatform\Doctrine\Orm\Paginator`, jamais d'array brut), échappatoire `?pagination=false`. Exclut les soft-deleted (RG-7.07). Tri `site.code ASC, storageType.label ASC, numero ASC`. Filtres `?search=` (numero), `?siteId[]=`, `?storageTypeId=`, `?state=`. Item : lookup par id, 404 si soft-deleted (§ 2.8). - **`StorageProcessor`** (Post/Patch) — `StorageFieldNormalizer` (numero → trim, **pas d'UPPER**, RG-7.06 / HP-M7-05) ; **RG-7.01** unicité `(site, storageType, numero)` parmi les actifs → **409** (exclut le courant en PATCH, filet anti-race sur l'index partiel) ; mode strict PATCH. - **Repository** étendu : `existsActiveBySiteTypeNumero()` + `createListQueryBuilder()` (jointures to-one site/storageType eager-load, filtres, JSONB `@>` pour `?state=`). - **Tests** : `AbstractStorageApiTestCase` + `StorageApiTest` (10 cas). ### Décision — RG-7.03 **omise** (validée avec Tristan) Le prompt demandait de vérifier que `storageType.sites` contient le site. Mais **ce concept a été retiré du modèle en M6** : la jointure `storage_type_site` a été droppée (migration `Version20260626100000`) et `StorageType` rendu *plat* — l'entité le documente explicitement (« un type n'est PAS rattaché à des sites ; la dispo relève de la future entité Stockage »). C'est désormais `Storage` (1 site + 1 type) qui **matérialise** cette disponibilité : il n'existe plus de référentiel à interroger. RG-7.03 est donc inimplémentable telle quelle et omise. **À reclarifier côté spec.** (De même, le filtre `?siteId[]=` du `StorageTypeProvider` cité par le prompt n'existe pas — supprimé en M6 ; le filtre site est porté ici directement par `Storage.site`.) ### Vérifications - `StorageApiTest` (10 tests, 64 assertions) : collection paginée Hydra + **contrat de sérialisation** (`site`/`storageType` objets embarqués, `displayName` présent) ; 201 création ; trim numéro ; **409** doublon triplet ; même numéro sur autre type → 201 ; réutilisation après soft-delete → 201 ; soft-deleted → 404 ; **view lit / ne gère pas** ; **403** des 4 personas métier (admin-only). - `make test` : `CollectionsArePaginatedTest` vert. Les échecs de la suite complète (Core/Commercial/Sites/Transport) sont du **flaky JWT + pollution BDD** (suite sans DAMA) — chacun passe sur BDD propre / en isolation ; les 4 rejoués **ensemble avec** `StorageApiTest` sur BDD propre = 38/38. - `make php-cs-fixer-allow-risky` : clean.
tristan added the type/featbackM7-Stockage labels 2026-06-29 14:43:49 +00:00
Author
Owner

Revue de code — M7 Stockages

🟠 storageTypeId=0 / siteId=0 : liste vide vs export completStorageProvider.php

ctype_digit('0') est vrai → le provider applique st.id = 0 (collection vide). L'export, lui, exige > 0 et traite 0 comme null (toutes les lignes). Même querystring, deux résultats. Racine commune avec les divergences de l'export (ERP-214) : les filtres sont parsés deux fois avec des règles subtilement différentes.

→ Factoriser un StorageListFilters (lecture unique, acceptant Request ou $context['filters']) consommé par le provider et le contrôleur d'export.

🟡 DuplicationStorageProcessor / StorageFieldNormalizer

StorageFieldNormalizer::normalizeNumero est byte-identique à ProductFieldNormalizer::normalizeName : un service entier (+ DI + dépendance processor + cast (string)) pour un trim() que Assert\NotBlank(normalizer:'trim') effectue déjà avant le processor. Les readers de filtres du provider sont aussi un copier-coller de ProductProvider. → Mutualiser dans src/Shared.

Faible (perf, hérité du pattern Product) : le filtre ?state= charge tous les ids matchés en PHP puis les réinjecte en IN (...) (deux allers-retours) au lieu d'un EXISTS / fonction DQL JSONB inline.

### Revue de code — M7 Stockages **🟠 `storageTypeId=0` / `siteId=0` : liste vide vs export complet** — `StorageProvider.php` `ctype_digit('0')` est vrai → le provider applique `st.id = 0` (collection vide). L'export, lui, exige `> 0` et traite 0 comme `null` (toutes les lignes). Même querystring, deux résultats. Racine commune avec les divergences de l'export (ERP-214) : **les filtres sont parsés deux fois** avec des règles subtilement différentes. → Factoriser un `StorageListFilters` (lecture unique, acceptant `Request` *ou* `$context['filters']`) consommé par le provider **et** le contrôleur d'export. **🟡 Duplication** — `StorageProcessor` / `StorageFieldNormalizer` `StorageFieldNormalizer::normalizeNumero` est byte-identique à `ProductFieldNormalizer::normalizeName` : un service entier (+ DI + dépendance processor + cast `(string)`) pour un `trim()` que `Assert\NotBlank(normalizer:'trim')` effectue déjà avant le processor. Les readers de filtres du provider sont aussi un copier-coller de `ProductProvider`. → Mutualiser dans `src/Shared`. _Faible (perf, hérité du pattern Product) : le filtre `?state=` charge tous les ids matchés en PHP puis les réinjecte en `IN (...)` (deux allers-retours) au lieu d'un `EXISTS` / fonction DQL JSONB inline._
tristan changed target branch from feat/erp-212-entite-storage to develop 2026-06-30 05:59:58 +00:00
tristan added 1 commit 2026-06-30 05:59:58 +00:00
tristan merged commit 0fe5b07d10 into develop 2026-06-30 06:00:01 +00:00
Sign in to join this conversation.