feat(catalog) : taxonomie FOURNISSEUR (type + filtre ?typeCode= + seed) (ERP-84) (#63)
Auto Tag Develop / tag (push) Successful in 8s
Auto Tag Develop / tag (push) Successful in 8s
## ERP-84 — Taxonomie FOURNISSEUR (Catalog) Prérequis du multi-select « Catégorie » de l'écran Ajouter fournisseur (#94) et de #92. Spec : `docs/specs/M2-suppliers/spec-back.md` § 2.4 + § 4.7. ### Contexte ERP-78 avait unifié la taxonomie sur un **type unique CLIENT** ; `GET /api/categories?typeCode=FOURNISSEUR` renvoyait alors les catégories CLIENT (filtre **ignoré**, un seul `CategoryType`). Le filtre `?typeCode=` n'existait pas en prod. ### Changements - **Filtre `?typeCode=` réel** sur `GET /api/categories` : `CategoryProvider` lit le filtre (même pattern que `includeDeleted`) et le passe à `DoctrineCategoryRepository::createListQueryBuilder`, qui joint le `CategoryType` et filtre sur son `code`. N'altère pas l'échappatoire `?pagination=false` ni la pagination Hydra. - **CategoryType FOURNISSEUR recréé** : migration racine `Version20260605120000` (`INSERT … ON CONFLICT` pour le type + 5 catégories de démo en `NOT EXISTS` : Négociant, Coopérative, Producteur, Grossiste, Importateur). Aucune colonne créée → pas de `COMMENT ON COLUMN`. - **Fixtures étendues** : `CategoryTypeFixtures` + `CategoryFixtures` seedent FOURNISSEUR de façon idempotente (survit à `make db-reset`). - **Test** : `CategoryTypeCodeFilterTest` (filtre exclusif, compat pagination Hydra, code inexistant → liste vide). ### Vérifications - `make php-cs-fixer-allow-risky` : clean. - `make test` : **483 tests OK** (1844 assertions). - Après `make db-reset` : - `/api/category_types` → `CLIENT` + `FOURNISSEUR`. - `?typeCode=FOURNISSEUR` → uniquement les 5 catégories FOURNISSEUR. - `?typeCode=CLIENT` → 11 catégories, type unique CLIENT. ### Critères d'acceptation - [x] `CategoryType` FOURNISSEUR présent après `make db-reset`. - [x] `?typeCode=FOURNISSEUR` ne renvoie QUE les catégories FOURNISSEUR. - [x] Catégories fournisseurs seedées sous ce type. - [x] `make test` vert. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #63 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #63.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <select>.
|
||||
@@ -97,4 +97,22 @@ final class CategoryProvider implements ProviderInterface
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lit le filtre `?typeCode=` depuis les filtres API Platform. Renvoie le code
|
||||
* normalise (trim) ou null si absent / vide. Ne contraint pas la casse : la
|
||||
* comparaison SQL se fait sur le code exact stocke (ex. FOURNISSEUR, CLIENT).
|
||||
*/
|
||||
private function readTypeCode(array $context): ?string
|
||||
{
|
||||
$raw = $context['filters']['typeCode'] ?? null;
|
||||
|
||||
if (!is_string($raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$raw = trim($raw);
|
||||
|
||||
return '' === $raw ? null : $raw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,14 +14,16 @@ use RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
/**
|
||||
* Fixtures dev/test du module Catalog : ~11 categories de demonstration, toutes
|
||||
* rattachees au type unique CLIENT (refonte taxonomie ERP-78). Chaque categorie
|
||||
* porte un `code` stable. Alimente le repertoire clients (ClientFixtures, module
|
||||
* Commercial) avec des donnees realistes couvrant RG-1.03 (codes DISTRIBUTEUR /
|
||||
* COURTIER) et RG-1.29 (codes interdits sur adresse).
|
||||
* Fixtures dev/test du module Catalog : categories de demonstration rattachees
|
||||
* a leur CategoryType. Le type CLIENT porte ~11 categories clients (refonte
|
||||
* taxonomie ERP-78) ; le type FOURNISSEUR porte les categories fournisseurs
|
||||
* (ERP-84 : Negociant, Cooperative...). Chaque categorie porte un `code` stable.
|
||||
* Alimente le repertoire clients (ClientFixtures, module Commercial) avec des
|
||||
* donnees realistes couvrant RG-1.03 (codes DISTRIBUTEUR / COURTIER) et RG-1.29
|
||||
* (codes interdits sur adresse), et le multi-select Categorie fournisseur (M2).
|
||||
*
|
||||
* Depend de CategoryTypeFixtures : le type CLIENT doit etre seede avant de
|
||||
* pouvoir y rattacher des Category.
|
||||
* Depend de CategoryTypeFixtures : les types CLIENT et FOURNISSEUR doivent etre
|
||||
* seedes avant de pouvoir y rattacher des Category.
|
||||
*
|
||||
* Idempotence : lookup par `code` parmi les categories non supprimees (deletedAt
|
||||
* null), coherent avec l'index unique partiel uq_category_code (code WHERE
|
||||
@@ -39,28 +41,36 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
*/
|
||||
class CategoryFixtures extends Fixture implements DependentFixtureInterface
|
||||
{
|
||||
/** Code du type unique (cf. CategoryTypeFixtures, migration ERP-78). */
|
||||
private const string CLIENT_TYPE_CODE = 'CLIENT';
|
||||
|
||||
/**
|
||||
* Source unique des categories de demonstration : nom => code stable. Les 4
|
||||
* premieres (Distributeur / Courtier / Secteur / Autre) sont les categories
|
||||
* Categories de demonstration par code de type. Les 4 premieres categories
|
||||
* CLIENT (Distributeur / Courtier / Secteur / Autre) sont les categories
|
||||
* « systeme » reportees des anciens types ; leurs codes pilotent les RG.
|
||||
* Les categories FOURNISSEUR (ERP-84) miroir de la migration
|
||||
* Version20260605120000. Chaque valeur : nom => code stable.
|
||||
*
|
||||
* @var array<string, string>
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private const CATEGORIES = [
|
||||
'Distributeur' => 'DISTRIBUTEUR',
|
||||
'Courtier' => 'COURTIER',
|
||||
'Secteur' => 'SECTEUR',
|
||||
'Autre' => 'AUTRE',
|
||||
'BTP' => 'BTP',
|
||||
'Industrie' => 'INDUSTRIE',
|
||||
'Agro-alimentaire' => 'AGRO_ALIMENTAIRE',
|
||||
'Transport/Logistique' => 'TRANSPORT_LOGISTIQUE',
|
||||
'Services' => 'SERVICES',
|
||||
'Association' => 'ASSOCIATION',
|
||||
'Indépendant' => 'INDEPENDANT',
|
||||
private const CATEGORIES_BY_TYPE = [
|
||||
'CLIENT' => [
|
||||
'Distributeur' => 'DISTRIBUTEUR',
|
||||
'Courtier' => 'COURTIER',
|
||||
'Secteur' => 'SECTEUR',
|
||||
'Autre' => 'AUTRE',
|
||||
'BTP' => 'BTP',
|
||||
'Industrie' => 'INDUSTRIE',
|
||||
'Agro-alimentaire' => 'AGRO_ALIMENTAIRE',
|
||||
'Transport/Logistique' => 'TRANSPORT_LOGISTIQUE',
|
||||
'Services' => 'SERVICES',
|
||||
'Association' => 'ASSOCIATION',
|
||||
'Indépendant' => 'INDEPENDANT',
|
||||
],
|
||||
'FOURNISSEUR' => [
|
||||
'Négociant' => 'NEGOCIANT',
|
||||
'Coopérative' => 'COOPERATIVE',
|
||||
'Producteur' => 'PRODUCTEUR',
|
||||
'Grossiste' => 'GROSSISTE',
|
||||
'Importateur' => 'IMPORTATEUR',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
@@ -84,31 +94,33 @@ class CategoryFixtures extends Fixture implements DependentFixtureInterface
|
||||
return;
|
||||
}
|
||||
|
||||
$clientType = null;
|
||||
// Index des types presents par code, pour rattacher chaque categorie.
|
||||
$typesByCode = [];
|
||||
foreach ($this->categoryTypeRepository->findAllOrderedByLabel() as $type) {
|
||||
if (self::CLIENT_TYPE_CODE === $type->getCode()) {
|
||||
$clientType = $type;
|
||||
$typesByCode[$type->getCode()] = $type;
|
||||
}
|
||||
|
||||
break;
|
||||
foreach (self::CATEGORIES_BY_TYPE as $typeCode => $categories) {
|
||||
$type = $typesByCode[$typeCode] ?? null;
|
||||
|
||||
if (!$type instanceof CategoryType) {
|
||||
// Misconfiguration : CategoryTypeFixtures n'a pas seede ce type.
|
||||
throw new RuntimeException(sprintf(
|
||||
'CategoryTypeFixtures doit avoir seede le type "%s" avant CategoryFixtures.',
|
||||
$typeCode,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (!$clientType instanceof CategoryType) {
|
||||
// Misconfiguration : CategoryTypeFixtures n'a pas tourne avant.
|
||||
throw new RuntimeException(
|
||||
'CategoryTypeFixtures doit avoir seede le type "CLIENT" avant CategoryFixtures.',
|
||||
);
|
||||
}
|
||||
|
||||
foreach (self::CATEGORIES as $name => $code) {
|
||||
$this->ensureCategory($manager, $name, $code, $clientType);
|
||||
foreach ($categories as $name => $code) {
|
||||
$this->ensureCategory($manager, $name, $code, $type);
|
||||
}
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cree la categorie (name, code) sous le type CLIENT si son code n'existe pas
|
||||
* Cree la categorie (name, code) sous le type fourni si son code n'existe pas
|
||||
* encore parmi les categories actives, sinon la laisse en place. Lookup
|
||||
* aligne sur l'index unique partiel uq_category_code.
|
||||
*/
|
||||
|
||||
@@ -12,10 +12,14 @@ use Doctrine\Persistence\ObjectManager;
|
||||
/**
|
||||
* Fixtures du module Catalog : seed du type de categorie (M1).
|
||||
*
|
||||
* Refonte taxonomie ERP-78 : le modele n'a plus qu'UN SEUL `category_type`,
|
||||
* CLIENT (code CLIENT, label « Client »). Distributeur / Courtier / Secteur /
|
||||
* Autre (et les categories metier fines) sont desormais des `Category` codees
|
||||
* rattachees a ce type (cf. CategoryFixtures + migration Version20260602100000).
|
||||
* Refonte taxonomie ERP-78 : le type CLIENT (code CLIENT, label « Client »)
|
||||
* porte les categories clients ; Distributeur / Courtier / Secteur / Autre (et
|
||||
* les categories metier fines) sont des `Category` codees rattachees a ce type
|
||||
* (cf. CategoryFixtures + migration Version20260602100000).
|
||||
*
|
||||
* ERP-84 : ajout du type FOURNISSEUR (code FOURNISSEUR, label « Fournisseur »),
|
||||
* taxonomie distincte des fournisseurs (Negociant, Cooperative...). Mirroir de
|
||||
* la migration Version20260605120000.
|
||||
*
|
||||
* Pourquoi une fixture EN PLUS du seed de la migration : `category_type` est une
|
||||
* entite managee par l ORM, donc le purger Doctrine la vide avant chaque
|
||||
@@ -31,11 +35,13 @@ use Doctrine\Persistence\ObjectManager;
|
||||
class CategoryTypeFixtures extends Fixture
|
||||
{
|
||||
/**
|
||||
* Source unique du type : code technique => libelle FR. Doit rester aligne
|
||||
* sur le seed de la migration Version20260602100000 (type unique CLIENT).
|
||||
* Source unique des types : code technique => libelle FR. Doit rester aligne
|
||||
* sur le seed des migrations Version20260602100000 (CLIENT) et
|
||||
* Version20260605120000 (FOURNISSEUR).
|
||||
*/
|
||||
private const TYPES = [
|
||||
'CLIENT' => 'Client',
|
||||
'CLIENT' => 'Client',
|
||||
'FOURNISSEUR' => 'Fournisseur',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -48,7 +48,7 @@ class DoctrineCategoryRepository extends ServiceEntityRepository implements Cate
|
||||
return [] !== $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function createListQueryBuilder(bool $includeDeleted = false): QueryBuilder
|
||||
public function createListQueryBuilder(bool $includeDeleted = false, ?string $typeCode = null): QueryBuilder
|
||||
{
|
||||
$qb = $this->createQueryBuilder('c')
|
||||
->orderBy('c.name', 'ASC')
|
||||
@@ -58,6 +58,16 @@ class DoctrineCategoryRepository extends ServiceEntityRepository implements Cate
|
||||
$qb->andWhere('c.deletedAt IS NULL');
|
||||
}
|
||||
|
||||
// Filtre `?typeCode=` : jointure sur le CategoryType pour ne garder que
|
||||
// les categories du type demande (ex. FOURNISSEUR). La jointure reste
|
||||
// compatible avec le Paginator ORM (fetchJoinCollection) du provider.
|
||||
if (null !== $typeCode) {
|
||||
$qb->join('c.categoryType', 'ct')
|
||||
->andWhere('ct.code = :typeCode')
|
||||
->setParameter('typeCode', $typeCode)
|
||||
;
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user