feat(catalog) : ERP-199 — entités Product + StorageType + repositories + contrat de sérialisation #150

Closed
matthieu wants to merge 1 commits from feat/erp-199-entites-product-storagetype into feat/erp-198-migration-schema-m6-produit
Owner

ERP-199 (1.3) — M6 Catalogue produit · Backend

Crée l'entité Product (+ #[ApiResource]) et le référentiel StorageType dans le module Catalog, avec le contrat de sérialisation posé une seule fois (read-groups par propriété affichée — RETEX M1→M5, 3 maillons spec § 4.0). Stackée sur ERP-198 (migration schéma).

Contenu

  • Product (#[Auditable], TimestampableBlamable, soft-delete préparé non exposé § 2.7) :
    • code (NotBlank, Length 50, unique global RG-6.01), name (Length 255), states (json multi-select PURCHASE/SALE/OTHER, ≥1 RG-6.02), manufactured/containsMolasses (RG-6.03), category ManyToOne (PRODUIT, RG-6.05), sites + storageTypes ManyToMany (≥1 — RG-6.04/6.06).
    • Messages FR sur toutes les contraintes, Length.max calé sur les colonnes ORM.
    • Opérations Get/GetCollection (catalog.products.view) + Post/Patch (...manage). Pas de Delete. Provider/Processor référencés (implémentés en ERP-200).
  • StorageType : référentiel statique lecture seule (code/label + sites ManyToMany), GetCollection + Get. Whitelisté dans EntitiesAreTimestampableBlamableTest::EXCLUDED (miroir CategoryType).
  • Repositories : interfaces Domain + implémentations Doctrine pour Product (dont existsActiveByCode PATCH-aware) et StorageType.

Points d'attention

  • Validation des états via Assert\\Choice(multiple: true) plutôt que Assert\\All([Choice]) : équivalent fonctionnel, mais seul Choice est géré par le garde-fou EntityConstraintsHaveFrenchMessageTest (All ferait échouer le test).
  • Workaround schema:update (tables désormais mappées) : les 5 tables M6 (product, storage_type, storage_type_site, product_site, product_storage_type) ajoutées à ColumnCommentsCatalog (sinon ColumnsHaveSqlCommentTest casse après schema:update --force), et l'index partiel uq_product_code_active rejoué dans la ligne dbal:run-sql du makefile test-db-setup.
  • states mappé type: 'json' : DBAL réintrospecte jsonbJSON → aucun diff destructeur (CHECK chk_product_states_not_empty préservé).
  • i18n audit.entity.catalog_product posée (Product est #[Auditable]).

Vérifications

  • make test-db-setup (migrate + schema:update + apply-column-comments + index partiel, sans erreur)
  • make test 873 tests, 6541 assertions (garde-fous Architecture verts)
  • make php-cs-fixer-allow-risky

Capture JSON réelle du contrat (DoD § 4.0.bis) = ERP-203. Provider/Processor = ERP-200.

## ERP-199 (1.3) — M6 Catalogue produit · Backend Crée l'entité `Product` (+ `#[ApiResource]`) et le référentiel `StorageType` dans le module **Catalog**, avec le contrat de sérialisation posé une seule fois (read-groups par propriété affichée — RETEX M1→M5, 3 maillons spec § 4.0). Stackée sur ERP-198 (migration schéma). ### Contenu - **Product** (`#[Auditable]`, TimestampableBlamable, soft-delete préparé non exposé § 2.7) : - `code` (NotBlank, Length 50, unique global RG-6.01), `name` (Length 255), `states` (json multi-select PURCHASE/SALE/OTHER, ≥1 RG-6.02), `manufactured`/`containsMolasses` (RG-6.03), `category` ManyToOne (PRODUIT, RG-6.05), `sites` + `storageTypes` ManyToMany (≥1 — RG-6.04/6.06). - Messages FR sur toutes les contraintes, `Length.max` calé sur les colonnes ORM. - Opérations Get/GetCollection (`catalog.products.view`) + Post/Patch (`...manage`). **Pas de Delete.** Provider/Processor référencés (implémentés en **ERP-200**). - **StorageType** : référentiel statique lecture seule (`code`/`label` + `sites` ManyToMany), GetCollection + Get. Whitelisté dans `EntitiesAreTimestampableBlamableTest::EXCLUDED` (miroir CategoryType). - **Repositories** : interfaces Domain + implémentations Doctrine pour Product (dont `existsActiveByCode` PATCH-aware) et StorageType. ### Points d'attention - **Validation des états** via `Assert\\Choice(multiple: true)` plutôt que `Assert\\All([Choice])` : équivalent fonctionnel, mais seul `Choice` est géré par le garde-fou `EntityConstraintsHaveFrenchMessageTest` (All ferait échouer le test). - **Workaround schema:update** (tables désormais mappées) : les 5 tables M6 (`product`, `storage_type`, `storage_type_site`, `product_site`, `product_storage_type`) ajoutées à `ColumnCommentsCatalog` (sinon `ColumnsHaveSqlCommentTest` casse après `schema:update --force`), et l'index partiel `uq_product_code_active` rejoué dans la ligne `dbal:run-sql` du makefile `test-db-setup`. - `states` mappé `type: 'json'` : DBAL réintrospecte `jsonb`→`JSON` → aucun diff destructeur (CHECK `chk_product_states_not_empty` préservé). - i18n `audit.entity.catalog_product` posée (Product est `#[Auditable]`). ### Vérifications - `make test-db-setup` ✅ (migrate + schema:update + apply-column-comments + index partiel, sans erreur) - `make test` ✅ **873 tests, 6541 assertions** (garde-fous Architecture verts) - `make php-cs-fixer-allow-risky` ✅ > Capture JSON réelle du contrat (DoD § 4.0.bis) = ERP-203. Provider/Processor = ERP-200.
matthieu added 1 commit 2026-06-25 08:45:06 +00:00
Entité Product (#[Auditable], TimestampableBlamable, soft-delete préparé non
exposé) et référentiel StorageType (lecture seule, provisoire) dans le module
Catalog, avec le contrat de sérialisation posé une fois (read-groups par
propriété affichée — RETEX M1→M5, 3 maillons spec § 4.0).

- Product : code (unique global RG-6.01), name, states (json multi-select
  PURCHASE/SALE/OTHER ≥1, RG-6.02), manufactured/containsMolasses (RG-6.03),
  category ManyToOne (PRODUIT, RG-6.05), sites + storageTypes ManyToMany (≥1).
  Messages FR sur toutes les contraintes, Length calée colonnes. Opérations
  Get/GetCollection (.view) + Post/Patch (.manage), pas de Delete. Provider/
  Processor référencés (implémentés en ERP-200).
- StorageType : code/label + sites ManyToMany (filtrage par site, ERP-201).
  Référentiel statique → whitelist EntitiesAreTimestampableBlamableTest.
- Repositories Product/StorageType (interfaces Domain + impl Doctrine).
- Validation états via Assert\Choice(multiple) plutôt qu'Assert\All (seul
  Choice est géré par EntityConstraintsHaveFrenchMessageTest).
- Garde-fous schema:update : 5 tables M6 ajoutées à ColumnCommentsCatalog,
  index partiel uq_product_code_active rejoué dans makefile test-db-setup.
- i18n audit.entity.catalog_product.
matthieu added the backM6-Produittype/feat labels 2026-06-25 08:45:27 +00:00
Author
Owner

Review back — ERP-199 (entités + repos + contrat sérialisation), read-only.

Périmètre : Product, StorageType, repositories, contrat de sérialisation.

Vérifié conforme :

  • declare(strict_types=1) partout ; commentaires FR.
  • #[Auditable] sur Product + clé i18n audit.entity.catalog_product présente (garde-fou AuditableEntitiesHaveI18nLabelTest vert). StorageType (référentiel) non auditable, whitelisté avec justification dans EntitiesAreTimestampableBlamableTest.
  • Product Timestampable/Blamable.
  • Messages #[Assert\*] FR explicites + Length.max == longueur colonne ORM (code 50, name 255) — garde-fou EntityConstraintsHaveFrenchMessageTest vert.
  • Contrat de sérialisation : category / sites / storageTypes embarqués (objets, pas IRI nus), states tableau, booléens présents.

Verdict : aucun retour obligatoire.

**Review back — ERP-199 (entités + repos + contrat sérialisation), read-only.** Périmètre : `Product`, `StorageType`, repositories, contrat de sérialisation. Vérifié conforme : - `declare(strict_types=1)` partout ; commentaires FR. - `#[Auditable]` sur `Product` + clé i18n `audit.entity.catalog_product` présente (garde-fou `AuditableEntitiesHaveI18nLabelTest` vert). `StorageType` (référentiel) non auditable, whitelisté avec justification dans `EntitiesAreTimestampableBlamableTest`. - `Product` Timestampable/Blamable. - Messages `#[Assert\*]` FR explicites + `Length.max` == longueur colonne ORM (`code` 50, `name` 255) — garde-fou `EntityConstraintsHaveFrenchMessageTest` vert. - Contrat de sérialisation : `category` / `sites` / `storageTypes` embarqués (objets, pas IRI nus), `states` tableau, booléens présents. 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.