feat(catalog) : ERP-202 — export XLSX du catalogue produits #153

Closed
matthieu wants to merge 1 commits from feat/erp-202-product-export-xlsx into feat/erp-201-storage-type-expose-seed
Owner

ERP-202 (1.6) — Export XLSX des produits

Endpoint GET /api/products/export.xlsx (controller Symfony custom, miroir ClientExportController / CarrierExportController).

Contenu

  • Route priority: 1 pour éviter le conflit API Platform /api/products/{id}.{_format} ; #[IsGranted('catalog.products.view')].
  • Colonnes : Numéro (code), Nom, États (Achat/Vendu/Autre joints), Catégorie, Sites, Types de stockage, Fabriqué, Contient mélasse.
  • Filtres identiques à la liste (ProductProvider) : ?search, ?categoryId / ?categoryCode, ?state, ?siteId[]. Produits soft-deleted toujours exclus (RG-6.09, § 2.7).
  • Génération via le helper Shared SpreadsheetExporterInterface (PhpSpreadsheet) ; Content-Type xlsx + Content-Disposition: attachment.

Tests

ProductExportControllerTest (7 tests, 51 assertions) : en-têtes + ligne de données, exclusion soft-delete, filtres ?search / ?state, peuplement de toutes les colonnes métier, 403 sans permission, 401 anonyme.

Note — étape 4 du ticket (whitelist CollectionsArePaginatedTest::EXCLUDED)

Non appliquée car non pertinente : ce garde-fou ne scanne que les opérations GetCollection des #[ApiResource]. L'export est un controller Symfony custom, jamais évalué par ce test — et EXCLUDED est vide alors que les exports Client/Provider/Carrier existent déjà. Ajouter une entrée serait du code mort. Suite verte sans whitelist.

Vérifications

  • make test FILES=tests/Module/Catalog/Api/ProductExportControllerTest.php → OK (7/51)
  • make test FILES="tests/Architecture tests/Module/Catalog" → OK (105/1015)
  • make php-cs-fixer-check → conforme

PR stackée sur feat/erp-201-storage-type-expose-seed.

## ERP-202 (1.6) — Export XLSX des produits Endpoint `GET /api/products/export.xlsx` (controller Symfony custom, miroir `ClientExportController` / `CarrierExportController`). ### Contenu - **Route** `priority: 1` pour éviter le conflit API Platform `/api/products/{id}.{_format}` ; `#[IsGranted('catalog.products.view')]`. - **Colonnes** : Numéro (`code`), Nom, États (Achat/Vendu/Autre joints), Catégorie, Sites, Types de stockage, Fabriqué, Contient mélasse. - **Filtres** identiques à la liste (`ProductProvider`) : `?search`, `?categoryId` / `?categoryCode`, `?state`, `?siteId[]`. Produits soft-deleted toujours exclus (RG-6.09, § 2.7). - **Génération** via le helper Shared `SpreadsheetExporterInterface` (PhpSpreadsheet) ; `Content-Type` xlsx + `Content-Disposition: attachment`. ### Tests `ProductExportControllerTest` (7 tests, 51 assertions) : en-têtes + ligne de données, exclusion soft-delete, filtres `?search` / `?state`, peuplement de toutes les colonnes métier, 403 sans permission, 401 anonyme. ### Note — étape 4 du ticket (whitelist `CollectionsArePaginatedTest::EXCLUDED`) Non appliquée car **non pertinente** : ce garde-fou ne scanne que les opérations `GetCollection` des `#[ApiResource]`. L'export est un controller Symfony custom, jamais évalué par ce test — et `EXCLUDED` est vide alors que les exports Client/Provider/Carrier existent déjà. Ajouter une entrée serait du code mort. Suite verte sans whitelist. ### Vérifications - `make test FILES=tests/Module/Catalog/Api/ProductExportControllerTest.php` → OK (7/51) - `make test FILES="tests/Architecture tests/Module/Catalog"` → OK (105/1015) - `make php-cs-fixer-check` → conforme > PR stackée sur `feat/erp-201-storage-type-expose-seed`.
matthieu added 1 commit 2026-06-25 09:55:10 +00:00
matthieu added the backM6-Produittype/feat labels 2026-06-25 09:55:28 +00:00
Author
Owner

Review back — ERP-202 (export XLSX), read-only.

Périmètre : GET /api/products/export.xlsx, export complet filtres appliqués.

Vérifié conforme :

  • Controller custom sous /api/ avec priority: 1 sur la route (évite le conflit API Platform {id}.{_format}).
  • #[IsGranted('catalog.products.view')].
  • Cloisonnement / filtres répliqués à l'identique du provider (mêmes filtres + exclusion soft-delete RG-6.09) avant matérialisation.
  • Génération déléguée à SpreadsheetExporterInterface (Shared) — pas d'import inter-module.
  • Test ProductExportControllerTest (extends AbstractCatalogApiTestCase, autonome) : scope + colonnes + enum états.

Verdict : aucun retour obligatoire.

**Review back — ERP-202 (export XLSX), read-only.** Périmètre : `GET /api/products/export.xlsx`, export complet filtres appliqués. Vérifié conforme : - Controller custom sous `/api/` avec `priority: 1` sur la route (évite le conflit API Platform `{id}.{_format}`). - `#[IsGranted('catalog.products.view')]`. - Cloisonnement / filtres **répliqués à l'identique** du provider (mêmes filtres + exclusion soft-delete RG-6.09) avant matérialisation. - Génération déléguée à `SpreadsheetExporterInterface` (Shared) — pas d'import inter-module. - Test `ProductExportControllerTest` (extends `AbstractCatalogApiTestCase`, autonome) : scope + colonnes + enum états. 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:09 +00:00

Pull request closed

Sign in to join this conversation.