[ERP-78] Refonte taxonomie Catégories : type unique CLIENT + Category.code + RG-1.03/1.29 par code (#42)
Auto Tag Develop / tag (push) Successful in 8s
Auto Tag Develop / tag (push) Successful in 8s
Refonte de la taxonomie Catégories (décision produit 01/06) : le modèle est inversé. ## Modèle - **UN SEUL `category_type` : CLIENT**. `Distributeur` / `Courtier` / `Secteur` / `Autre` (+ catégories métier) deviennent des `Category` rattachées à CLIENT. - Filtrage métier sur un **`code` stable porté par `Category`** (NOT NULL, unique partiel `uq_category_code`), slug MAJUSCULE auto-généré du nom (`CategoryCodeGenerator`), figé à la création, exposé en **lecture seule**. ## Contenu - **M0** : `Category.code` (entité + migration corrective `Version20260602100000` au namespace racine + `COMMENT ON COLUMN` + catalogue + ligne `test-db-setup`). Retrofit `Version20260528120000` rendu conscient des colonnes. - **Seed** : type unique CLIENT, catégories codées (`Distributeur→DISTRIBUTEUR`, etc.), anciens types supprimés. Fixtures `CategoryType`/`Category`/`Client` alignées. - **RG-1.03** : `ClientProcessor::hasCategoryCode` — un distributor/broker doit porter la `Category` de code `DISTRIBUTEUR`/`COURTIER`. Filtre liste/export `categoryType` → `categoryCode`. - **RG-1.29** : `ClientAddress::validateCategoryCodes` — denylist des codes `DISTRIBUTEUR`/`COURTIER` sur une adresse (toute autre catégorie autorisée). - **Specs** M0/M1 (back + front) amendées. ## Tests `make php-cs-fixer-allow-risky` OK ; `make db-reset` OK (type unique CLIENT + 11 catégories codées, idempotent) ; `make test` **443 vert**. Ajouts : RG-1.03 courtier, génération/unicité/lecture-seule du code (`CategoryCodeTest`). ## Coordination - #76 (#500) : RG-1.29 réécrite ici sur le nouveau modèle ; #76 ne garde que le gap 2 (mapping CHECK adresse → 422), indépendant de la taxonomie. - ERP-68 (#486) : fixtures démo (déjà mergées via #41) adaptées ici au type unique CLIENT + codes. - Front #480–483 : selects Catégorie / distributeur / courtier basés sur le `code` (`?categoryCode=`), plus le type. Décisions actées avec le PO : `code` NOT NULL auto-généré (slug) ; périmètre complet (réécriture RG + fixtures déjà mergées). --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #42 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 #42.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Catalog\Application\Service;
|
||||
|
||||
use App\Module\Catalog\Domain\Repository\CategoryRepositoryInterface;
|
||||
use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||
|
||||
/**
|
||||
* Genere le code technique stable d'une Category a partir de son nom (ERP-78).
|
||||
*
|
||||
* Regle (decision produit 02/06) : `code` est obligatoire et auto-genere — un
|
||||
* slug MAJUSCULE du nom, sans accent, separateurs non alphanumeriques reduits a
|
||||
* `_`, borne a 50 caracteres (longueur colonne). Exemples :
|
||||
* - « Distributeur » -> DISTRIBUTEUR
|
||||
* - « Agro-alimentaire » -> AGRO_ALIMENTAIRE
|
||||
* - « Transport/Logistique » -> TRANSPORT_LOGISTIQUE
|
||||
*
|
||||
* Le code est FIGE a la creation (jamais recalcule sur renommage) afin de rester
|
||||
* une cle deterministe stable entre environnements (RG-1.03 / RG-1.29 cote M1).
|
||||
*
|
||||
* Unicite : l'index partiel `uq_category_code` (WHERE deleted_at IS NULL) impose
|
||||
* l'unicite parmi les categories actives. Deux noms distincts peuvent produire
|
||||
* le meme slug (« Agro alimentaire » / « Agro-alimentaire ») : on suffixe alors
|
||||
* le code par `_2`, `_3`... jusqu'a obtenir un code libre.
|
||||
*/
|
||||
final class CategoryCodeGenerator
|
||||
{
|
||||
/** Longueur maximale de la colonne `category.code`. */
|
||||
private const int MAX_LENGTH = 50;
|
||||
|
||||
private readonly AsciiSlugger $slugger;
|
||||
|
||||
public function __construct(
|
||||
private readonly CategoryRepositoryInterface $categoryRepository,
|
||||
) {
|
||||
$this->slugger = new AsciiSlugger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slug brut (sans garantie d'unicite) — utile pour les seeds deterministes.
|
||||
*/
|
||||
public function slugify(string $name): string
|
||||
{
|
||||
$slug = $this->slugger->slug($name, '_')->upper()->toString();
|
||||
|
||||
// Borne a la longueur colonne, puis retire un eventuel `_` terminal
|
||||
// introduit par la troncature.
|
||||
$slug = substr($slug, 0, self::MAX_LENGTH);
|
||||
$slug = trim($slug, '_');
|
||||
|
||||
// Garde-fou : un nom uniquement compose de caracteres non alphanumeriques
|
||||
// (theorique, le nom est NotBlank + Length>=2) donnerait un slug vide.
|
||||
return '' === $slug ? 'CATEGORY' : $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code unique parmi les categories actives : slug du nom, suffixe `_N` en
|
||||
* cas de collision. `$excludeId` ignore la categorie courante (PATCH).
|
||||
*/
|
||||
public function generateUnique(string $name, ?int $excludeId = null): string
|
||||
{
|
||||
$base = $this->slugify($name);
|
||||
$candidate = $base;
|
||||
$suffix = 2;
|
||||
|
||||
while ($this->categoryRepository->existsActiveByCode($candidate, $excludeId)) {
|
||||
$suffixStr = '_'.$suffix;
|
||||
// Retronque la base pour que `base + suffixe` tienne dans 50 caracteres.
|
||||
$candidate = substr($base, 0, self::MAX_LENGTH - strlen($suffixStr)).$suffixStr;
|
||||
++$suffix;
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
@@ -74,10 +74,11 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: DoctrineCategoryRepository::class)]
|
||||
#[ORM\Table(name: 'category')]
|
||||
// Index nommes pour matcher la migration (cf. Role/Permission/Site). L'index
|
||||
// unique partiel `uq_category_name_type_active` (LOWER(name), category_type_id
|
||||
// WHERE deleted_at IS NULL) reste possede par la seule migration : Doctrine ORM
|
||||
// ne sait pas exprimer un index fonctionnel + partiel via attribut.
|
||||
// Index nommes pour matcher la migration (cf. Role/Permission/Site). Les index
|
||||
// uniques partiels `uq_category_name_type_active` (LOWER(name), category_type_id
|
||||
// WHERE deleted_at IS NULL) et `uq_category_code` (code WHERE deleted_at IS NULL)
|
||||
// restent possedes par la seule migration : Doctrine ORM ne sait pas exprimer un
|
||||
// index partiel via attribut.
|
||||
#[ORM\Index(name: 'idx_category_deleted_at', columns: ['deleted_at'])]
|
||||
#[ORM\Index(name: 'idx_category_type_id', columns: ['category_type_id'])]
|
||||
#[ORM\Index(name: 'idx_category_created_by', columns: ['created_by'])]
|
||||
@@ -109,6 +110,16 @@ class Category implements TimestampableInterface, BlamableInterface, CategoryInt
|
||||
#[Groups(['category:read', 'category:write'])]
|
||||
private ?string $name = null;
|
||||
|
||||
// Code technique stable (slug MAJUSCULE du nom) — NOT NULL + unique parmi les
|
||||
// actifs (index partiel `uq_category_code` possede par la migration). Genere
|
||||
// par le CategoryProcessor a la creation puis fige (jamais recalcule sur
|
||||
// renommage) : sert de cle metier deterministe (RG-1.03 / RG-1.29). Lecture
|
||||
// seule cote API (hors groupe category:write) : le front filtre dessus mais
|
||||
// ne le saisit pas.
|
||||
#[ORM\Column(length: 50)]
|
||||
#[Groups(['category:read'])]
|
||||
private ?string $code = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: CategoryType::class)]
|
||||
#[ORM\JoinColumn(name: 'category_type_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
|
||||
#[Assert\NotNull(message: 'Type de catégorie obligatoire.')]
|
||||
@@ -141,6 +152,21 @@ class Category implements TimestampableInterface, BlamableInterface, CategoryInt
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemente CategoryInterface : code technique stable de la categorie.
|
||||
*/
|
||||
public function getCode(): ?string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setCode(string $code): static
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCategoryType(): ?CategoryType
|
||||
{
|
||||
return $this->categoryType;
|
||||
|
||||
@@ -13,6 +13,13 @@ interface CategoryRepositoryInterface
|
||||
|
||||
public function save(Category $category): void;
|
||||
|
||||
/**
|
||||
* Vrai si une categorie active (deleted_at IS NULL) porte deja ce code.
|
||||
* `$excludeId` exclut une categorie precise du test (cas PATCH). Sert a
|
||||
* garantir l'unicite du code generee par le CategoryCodeGenerator (ERP-78).
|
||||
*/
|
||||
public function existsActiveByCode(string $code, ?int $excludeId = null): bool;
|
||||
|
||||
/**
|
||||
* Construit un QueryBuilder de liste avec filtre soft-delete et tri par defaut.
|
||||
* - $includeDeleted = false : exclut les categories soft-deleted (RG-1.08)
|
||||
|
||||
+17
-4
@@ -7,6 +7,7 @@ namespace App\Module\Catalog\Infrastructure\ApiPlatform\State\Processor;
|
||||
use ApiPlatform\Metadata\DeleteOperationInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Module\Catalog\Application\Service\CategoryCodeGenerator;
|
||||
use App\Module\Catalog\Domain\Entity\Category;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
@@ -16,10 +17,13 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
/**
|
||||
* Processor Category : applique les regles de gestion en ecriture.
|
||||
*
|
||||
* - POST / PATCH : trim du nom (RG-1.03) puis delegation au persist_processor
|
||||
* Doctrine ORM. Toute UniqueConstraintViolationException remontee par Postgres
|
||||
* (collision sur l'index partiel uq_category_name_type_active) est traduite
|
||||
* en HTTP 409 avec le message attendu par la spec (RG-1.07).
|
||||
* - POST / PATCH : trim du nom (RG-1.03) ; a la CREATION, generation du `code`
|
||||
* technique stable (slug MAJUSCULE du nom, unique parmi les actifs — ERP-78)
|
||||
* via CategoryCodeGenerator ; puis delegation au persist_processor Doctrine
|
||||
* ORM. Le code est FIGE a la creation (jamais recalcule sur PATCH). Toute
|
||||
* UniqueConstraintViolationException remontee par Postgres (collision sur
|
||||
* l'index partiel uq_category_name_type_active) est traduite en HTTP 409 avec
|
||||
* le message attendu par la spec (RG-1.07).
|
||||
* - DELETE : soft delete (RG-1.12). On NE delegue PAS au remove_processor ;
|
||||
* on pose deletedAt = now() puis on delegue au persist_processor pour que
|
||||
* le UPDATE Doctrine parte et que le TimestampableBlamableSubscriber mette
|
||||
@@ -32,6 +36,7 @@ final class CategoryProcessor implements ProcessorInterface
|
||||
public function __construct(
|
||||
#[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')]
|
||||
private readonly ProcessorInterface $persistProcessor,
|
||||
private readonly CategoryCodeGenerator $codeGenerator,
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
@@ -62,6 +67,14 @@ final class CategoryProcessor implements ProcessorInterface
|
||||
$data->setName(trim($data->getName()));
|
||||
}
|
||||
|
||||
// ERP-78 : le code est genere a la CREATION puis fige. On le (re)genere
|
||||
// uniquement s'il est absent (POST, ou entite seedee sans code) — un PATCH
|
||||
// sur une categorie existante conserve son code. Genere depuis le nom
|
||||
// (NotBlank, deja trimme), unique parmi les actifs.
|
||||
if (null === $data->getCode() && null !== $data->getName()) {
|
||||
$data->setCode($this->codeGenerator->generateUnique($data->getName(), $data->getId()));
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
} catch (UniqueConstraintViolationException $e) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -31,6 +31,23 @@ class DoctrineCategoryRepository extends ServiceEntityRepository implements Cate
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
public function existsActiveByCode(string $code, ?int $excludeId = null): bool
|
||||
{
|
||||
$qb = $this->createQueryBuilder('c')
|
||||
->select('1')
|
||||
->andWhere('c.code = :code')
|
||||
->andWhere('c.deletedAt IS NULL')
|
||||
->setParameter('code', $code)
|
||||
->setMaxResults(1)
|
||||
;
|
||||
|
||||
if (null !== $excludeId) {
|
||||
$qb->andWhere('c.id != :excludeId')->setParameter('excludeId', $excludeId);
|
||||
}
|
||||
|
||||
return [] !== $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function createListQueryBuilder(bool $includeDeleted = false): QueryBuilder
|
||||
{
|
||||
$qb = $this->createQueryBuilder('c')
|
||||
|
||||
Reference in New Issue
Block a user