From 92a2d4f763a735d0e864d4f88184e466b4979e60 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 5 Jun 2026 09:59:37 +0200 Subject: [PATCH] feat(catalog) : taxonomie FOURNISSEUR (type + filtre ?typeCode= + seed) (ERP-84) Recree le CategoryType FOURNISSEUR (unifie sur CLIENT par ERP-78) et implemente un vrai filtre ?typeCode= sur GET /api/categories (inexistant en prod). - CategoryProvider lit ?typeCode= depuis les filtres (meme pattern que includeDeleted) et le passe au repository ; naltere pas ?pagination=false. - DoctrineCategoryRepository::createListQueryBuilder joint le CategoryType et filtre sur son code (compatible Paginator ORM fetchJoinCollection). - Migration racine Version20260605120000 : seed du type FOURNISSEUR en ON CONFLICT + 5 categories de demo (Negociant, Cooperative, Producteur, Grossiste, Importateur) en NOT EXISTS. Aucune colonne creee. - CategoryTypeFixtures / CategoryFixtures etendus a FOURNISSEUR (idempotent, survit a make db-reset). - Test CategoryTypeCodeFilterTest : filtre exclusif, compat pagination Hydra, code inexistant -> liste vide. --- migrations/Version20260605120000.php | 102 ++++++++++++++++++ .../CategoryRepositoryInterface.php | 5 +- .../State/Provider/CategoryProvider.php | 20 +++- .../DataFixtures/CategoryFixtures.php | 92 +++++++++------- .../DataFixtures/CategoryTypeFixtures.php | 20 ++-- .../Doctrine/DoctrineCategoryRepository.php | 12 ++- .../Api/CategoryTypeCodeFilterTest.php | 86 +++++++++++++++ 7 files changed, 287 insertions(+), 50 deletions(-) create mode 100644 migrations/Version20260605120000.php create mode 100644 tests/Module/Catalog/Api/CategoryTypeCodeFilterTest.php diff --git a/migrations/Version20260605120000.php b/migrations/Version20260605120000.php new file mode 100644 index 0000000..6814005 --- /dev/null +++ b/migrations/Version20260605120000.php @@ -0,0 +1,102 @@ + pas de `COMMENT ON COLUMN` (regle ABSOLUE n°12). + * + * Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire + * Catalog : avec plusieurs migrations_paths, Doctrine Migrations 3.x trie par + * FQCN alphabetique -> une migration `App\Module\Catalog\...` passerait avant les + * `DoctrineMigrations\...` sur base vide, donc avant la creation de la table + * `category_type`. Le namespace racine garantit l'ordre par timestamp. + * + * Idempotence : `INSERT ... ON CONFLICT (code) DO NOTHING` pour le type, + * `INSERT ... SELECT ... WHERE NOT EXISTS` pour chaque categorie (aligne sur le + * pattern ERP-78 etape 4). En prod la table `category` est vide (aucune fixture + * metier). En dev/test, le purger Doctrine vide `category`/`category_type` avant + * les fixtures qui reproduisent le meme etat final (CategoryTypeFixtures / + * CategoryFixtures etendus a FOURNISSEUR). + */ +final class Version20260605120000 extends AbstractMigration +{ + /** + * Categories de demonstration du type FOURNISSEUR : nom => code stable. Le + * code est la cle metier (slug MAJUSCULE du nom, miroir du + * CategoryCodeGenerator) et reste unique parmi les actifs (uq_category_code, + * partage avec les codes CLIENT — aucune collision ici). + */ + private const array SUPPLIER_CATEGORIES = [ + 'Négociant' => 'NEGOCIANT', + 'Coopérative' => 'COOPERATIVE', + 'Producteur' => 'PRODUCTEUR', + 'Grossiste' => 'GROSSISTE', + 'Importateur' => 'IMPORTATEUR', + ]; + + public function getDescription(): string + { + return 'ERP-84 : recree le CategoryType FOURNISSEUR + seed des categories fournisseurs (Negociant, Cooperative...).'; + } + + public function up(Schema $schema): void + { + // 1. Type FOURNISSEUR (idempotent via l'index unique uq_category_type_code). + $this->addSql(<<<'SQL' + INSERT INTO category_type (code, label) VALUES ('FOURNISSEUR', 'Fournisseur') + ON CONFLICT (code) DO NOTHING + SQL); + + // 2. Categories de demonstration sous FOURNISSEUR (si le code est libre + // parmi les actifs). created_at/updated_at NOT NULL -> NOW() ; le blame + // reste null (seed hors contexte HTTP, libelle « Systeme » cote front). + foreach (self::SUPPLIER_CATEGORIES as $name => $code) { + $this->addSql(<<<'SQL' + INSERT INTO category (name, code, category_type_id, created_at, updated_at) + SELECT :name, :code, ct.id, NOW(), NOW() + FROM category_type ct + WHERE ct.code = 'FOURNISSEUR' + AND NOT EXISTS ( + SELECT 1 FROM category c WHERE c.code = :code AND c.deleted_at IS NULL + ) + SQL, ['name' => $name, 'code' => $code]); + } + } + + public function down(Schema $schema): void + { + // Best-effort : on retire d'abord les categories seedees (par code), puis + // le type s'il n'est plus reference (guard NOT EXISTS sur la FK RESTRICT). + $this->addSql( + 'DELETE FROM category WHERE code IN (:codes) ' + ."AND category_type_id = (SELECT id FROM category_type WHERE code = 'FOURNISSEUR')", + ['codes' => array_values(self::SUPPLIER_CATEGORIES)], + ['codes' => \Doctrine\DBAL\ArrayParameterType::STRING], + ); + + $this->addSql(<<<'SQL' + DELETE FROM category_type + WHERE code = 'FOURNISSEUR' + AND NOT EXISTS ( + SELECT 1 FROM category c WHERE c.category_type_id = category_type.id + ) + SQL); + } +} diff --git a/src/Module/Catalog/Domain/Repository/CategoryRepositoryInterface.php b/src/Module/Catalog/Domain/Repository/CategoryRepositoryInterface.php index 5a43d5c..60f7eb8 100644 --- a/src/Module/Catalog/Domain/Repository/CategoryRepositoryInterface.php +++ b/src/Module/Catalog/Domain/Repository/CategoryRepositoryInterface.php @@ -23,7 +23,10 @@ interface CategoryRepositoryInterface /** * Construit un QueryBuilder de liste avec filtre soft-delete et tri par defaut. * - $includeDeleted = false : exclut les categories soft-deleted (RG-1.08) + * - $typeCode non null : ne garde que les categories dont le CategoryType + * porte ce code (filtre `?typeCode=`, ex. FOURNISSEUR / CLIENT). Sert au + * multi-select Categorie du fournisseur (M2, RG-2.10). * - Tri : name ASC (RG-1.10). */ - public function createListQueryBuilder(bool $includeDeleted = false): QueryBuilder; + public function createListQueryBuilder(bool $includeDeleted = false, ?string $typeCode = null): QueryBuilder; } diff --git a/src/Module/Catalog/Infrastructure/ApiPlatform/State/Provider/CategoryProvider.php b/src/Module/Catalog/Infrastructure/ApiPlatform/State/Provider/CategoryProvider.php index bc7f4c7..25fc1b5 100644 --- a/src/Module/Catalog/Infrastructure/ApiPlatform/State/Provider/CategoryProvider.php +++ b/src/Module/Catalog/Infrastructure/ApiPlatform/State/Provider/CategoryProvider.php @@ -40,7 +40,7 @@ final class CategoryProvider implements ProviderInterface $includeDeleted = $this->readIncludeDeleted($context); if ($operation instanceof CollectionOperationInterface) { - $qb = $this->repository->createListQueryBuilder($includeDeleted); + $qb = $this->repository->createListQueryBuilder($includeDeleted, $this->readTypeCode($context)); // Echappatoire ?pagination=false : retourne la collection complete sans Paginator. // Utile pour les drawers Role/Permission/Site/CategoryType qui alimentent un