feat(catalog) : single CLIENT category type + corrective migration

Migration corrective Version20260602100000 (namespace racine) : ajoute
Category.code, cree le type unique CLIENT, reporte Distributeur/Courtier/
Secteur/Autre en Category codees sous CLIENT, supprime les anciens types.
Fixtures alignees (type unique CLIENT, categories codees). Index partiel
uq_category_code recree dans test-db-setup.
This commit is contained in:
Matthieu
2026-06-02 09:20:58 +02:00
parent 13bb51ee9a
commit 636f2ccb8e
4 changed files with 256 additions and 73 deletions
@@ -14,19 +14,19 @@ use RuntimeException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* Fixtures dev/test du module Catalog : ~12 categories de demonstration reparties
* sur les 4 types metier (DISTRIBUTEUR / COURTIER / SECTEUR / AUTRE). Alimente le
* repertoire clients (ClientFixtures, module Commercial) avec des donnees
* realistes couvrant les categorisations RG-1.03 (DISTRIBUTEUR/COURTIER) et
* RG-1.29 (SECTEUR/AUTRE sur adresse).
* 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).
*
* Depend de CategoryTypeFixtures : les 4 CategoryType doivent etre seedes avant
* de pouvoir y rattacher des Category.
* Depend de CategoryTypeFixtures : le type CLIENT doit etre seede avant de
* pouvoir y rattacher des Category.
*
* Idempotence : lookup par (name, categoryType) parmi les categories non
* supprimees (deletedAt null), coherent avec l'index unique partiel
* uq_category_name_type_active (LOWER(name), category_type_id WHERE deleted_at
* IS NULL). Rejouable sans doublon meme si le purger Doctrine est desactive.
* Idempotence : lookup par `code` parmi les categories non supprimees (deletedAt
* null), coherent avec l'index unique partiel uq_category_code (code WHERE
* deleted_at IS NULL). Rejouable sans doublon meme si le purger Doctrine est
* desactive.
*
* Audit / Blamable : persist hors contexte HTTP -> created_by / updated_by
* restent null (« Systeme » cote front), c'est attendu.
@@ -34,39 +34,33 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
* Portee : DONNEES DE DEMONSTRATION (dev uniquement). En environnement `test`,
* la fixture ne charge rien : les tests seedent et nettoient leurs propres
* categories (prefixe dedie) et comptent sur une table `category` vierge — y
* injecter 12 categories de demo casserait comptages et cleanups FK
* injecter des categories de demo casserait comptages et cleanups FK
* (client_category). Cf. ClientFixtures (meme garde-fou).
*/
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 : code de type metier =>
* liste de noms. Les noms sont stockes tels quels (l'unicite est
* case-insensitive cote index).
* Source unique des categories de demonstration : nom => code stable. Les 4
* premieres (Distributeur / Courtier / Secteur / Autre) sont les categories
* « systeme » reportees des anciens types ; leurs codes pilotent les RG.
*
* @var array<string, list<string>>
* @var array<string, string>
*/
private const CATEGORIES = [
'SECTEUR' => [
'BTP',
'Industrie',
'Agro-alimentaire',
'Transport/Logistique',
'Services',
],
'DISTRIBUTEUR' => [
'Distributeur Grand Sud-Ouest',
'Distributeur National Premium',
'Grossiste régional',
],
'COURTIER' => [
'Cabinet de courtage Léonard',
'Cabinet de courtage Bernard',
],
'AUTRE' => [
'Indépendant',
'Association',
],
'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',
];
public function __construct(
@@ -90,41 +84,39 @@ class CategoryFixtures extends Fixture implements DependentFixtureInterface
return;
}
// Index des types metier par code (CategoryTypeFixtures les a seedes).
$typesByCode = [];
$clientType = null;
foreach ($this->categoryTypeRepository->findAllOrderedByLabel() as $type) {
$typesByCode[$type->getCode()] = $type;
if (self::CLIENT_TYPE_CODE === $type->getCode()) {
$clientType = $type;
break;
}
}
foreach (self::CATEGORIES as $typeCode => $names) {
$type = $typesByCode[$typeCode] ?? null;
if (!$type instanceof CategoryType) {
// Misconfiguration : CategoryTypeFixtures n'a pas tourne avant.
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 ($names as $name) {
$this->ensureCategory($manager, $name, $type);
}
foreach (self::CATEGORIES as $name => $code) {
$this->ensureCategory($manager, $name, $code, $clientType);
}
$manager->flush();
}
/**
* Cree la categorie (name, type) si elle n'existe pas encore parmi les
* categories actives, sinon la laisse en place. Lookup aligne sur l'index
* unique partiel (nom + type, hors soft-deleted).
* Cree la categorie (name, code) sous le type CLIENT 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.
*/
private function ensureCategory(ObjectManager $manager, string $name, CategoryType $type): void
private function ensureCategory(ObjectManager $manager, string $name, string $code, CategoryType $type): void
{
$existing = $manager->getRepository(Category::class)->findOneBy([
'name' => $name,
'categoryType' => $type,
'deletedAt' => null,
'code' => $code,
'deletedAt' => null,
]);
if (null !== $existing) {
@@ -133,6 +125,7 @@ class CategoryFixtures extends Fixture implements DependentFixtureInterface
$category = new Category();
$category->setName($name);
$category->setCode($code);
$category->setCategoryType($type);
$manager->persist($category);
}
@@ -10,17 +10,19 @@ use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
/**
* Fixtures du module Catalog : seed des types de categorie metier (M1).
* Fixtures du module Catalog : seed du type de categorie (M1).
*
* La table `category_type` est creee vide au M0 ; le M1 la peuple avec les 4
* types DISTRIBUTEUR / COURTIER / SECTEUR / AUTRE (cf. spec M1 § 3.3).
* 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).
*
* Pourquoi une fixture EN PLUS du seed de la migration (Version20260601000000) :
* `category_type` est une entite managee par l ORM, donc le purger Doctrine la
* vide avant chaque `doctrine:fixtures:load`. Sans cette fixture, les 4 types
* seedes par la migration disparaitraient apres `make db-reset` / setup de test.
* Le seed migration couvre la prod (ou les fixtures ne tournent pas) ; cette
* fixture re-aligne dev et test. Les deux chemins produisent un etat identique.
* 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
* `doctrine:fixtures:load`. Sans cette fixture, le type CLIENT seede par la
* migration disparaitrait apres `make db-reset` / setup de test. Le seed
* migration couvre la prod (ou les fixtures ne tournent pas) ; cette fixture
* re-aligne dev et test. Les deux chemins produisent un etat identique.
*
* Idempotence : lookup par `code` parmi les types existants avant insertion,
* sur le modele d AppFixtures::ensureSystemRole. Rejouable sans doublon meme
@@ -29,14 +31,11 @@ use Doctrine\Persistence\ObjectManager;
class CategoryTypeFixtures extends Fixture
{
/**
* Source unique des 4 types metier : code technique => libelle FR.
* Doit rester aligne sur le seed de la migration Version20260601000000.
* Source unique du type : code technique => libelle FR. Doit rester aligne
* sur le seed de la migration Version20260602100000 (type unique CLIENT).
*/
private const TYPES = [
'DISTRIBUTEUR' => 'Distributeur',
'COURTIER' => 'Courtier',
'SECTEUR' => 'Secteur',
'AUTRE' => 'Autre',
'CLIENT' => 'Client',
];
public function __construct(