[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:
@@ -83,6 +83,9 @@ abstract class AbstractCatalogApiTestCase extends AbstractApiTestCase
|
||||
$suffix = substr(bin2hex(random_bytes(4)), 0, 8);
|
||||
$category = new Category();
|
||||
$category->setName($name ?? self::TEST_CATEGORY_PREFIX.$suffix);
|
||||
// ERP-78 : code NOT NULL + unique parmi les actifs (uq_category_code).
|
||||
// Nonce aleatoire -> unicite garantie entre seeds successifs du test.
|
||||
$category->setCode('TEST_'.strtoupper($suffix));
|
||||
$category->setCategoryType($type);
|
||||
if (null !== $deletedAt) {
|
||||
$category->setDeletedAt($deletedAt);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Catalog\Api;
|
||||
|
||||
use App\Module\Catalog\Application\Service\CategoryCodeGenerator;
|
||||
use App\Shared\Infrastructure\Database\CategoryCodeSql;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* Garde-fou ERP-78 : l'expression SQL de slug (CategoryCodeSql, utilisee par le
|
||||
* backfill de la migration Version20260602100000) doit produire EXACTEMENT le
|
||||
* meme code que le generateur applicatif (CategoryCodeGenerator::slugify), sur
|
||||
* tout le domaine de noms francais / Latin-1.
|
||||
*
|
||||
* Verrouille la cause racine du bug initial : deux implementations d'un meme
|
||||
* slug qui derivent silencieusement (« Independant » -> IND_PENDANT cote SQL
|
||||
* faute de translitteration des accents, vs INDEPENDANT cote PHP). On ne couvre
|
||||
* volontairement PAS les ligatures (`Œ`, `ß`) : `translate()` est 1->1 et ne
|
||||
* peut produire `OE`/`SS` ; elles sont hors du domaine des categories CLIENT.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CategoryCodeSqlSlugTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* Noms representatifs du domaine reel : accents, cedille, apostrophe,
|
||||
* separateurs varies, parentheses, majuscules accentuees.
|
||||
*
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function nameProvider(): iterable
|
||||
{
|
||||
yield 'sans accent' => ['Distributeur'];
|
||||
yield 'tiret' => ['Agro-alimentaire'];
|
||||
yield 'slash' => ['Transport/Logistique'];
|
||||
yield 'accent aigu' => ['Indépendant'];
|
||||
yield 'apostrophe + accent' => ["L'Oréal"];
|
||||
yield 'esperluette' => ['Forêt & Bûcheron'];
|
||||
yield 'cedille majuscule' => ['Ça va'];
|
||||
yield 'accents multiples' => ['Naïve façade'];
|
||||
yield 'circonflexe' => ["Côte d'Azur"];
|
||||
yield 'parentheses' => ['Zone (Sud)'];
|
||||
}
|
||||
|
||||
#[DataProvider('nameProvider')]
|
||||
public function testSqlSlugMatchesPhpSlug(string $name): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$container = self::getContainer();
|
||||
|
||||
/** @var Connection $conn */
|
||||
$conn = $container->get('doctrine')->getConnection();
|
||||
/** @var CategoryCodeGenerator $generator */
|
||||
$generator = $container->get(CategoryCodeGenerator::class);
|
||||
|
||||
// Evaluation pure de l'expression (aucune table requise) : le nom est
|
||||
// passe en parametre lie a la place de la colonne.
|
||||
$sqlSlug = $conn->fetchOne(
|
||||
'SELECT '.CategoryCodeSql::slugExpression(':name'),
|
||||
['name' => $name],
|
||||
);
|
||||
|
||||
self::assertSame(
|
||||
$generator->slugify($name),
|
||||
$sqlSlug,
|
||||
sprintf('SQL et PHP doivent produire le meme slug pour "%s".', $name),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Catalog\Api;
|
||||
|
||||
/**
|
||||
* Tests ERP-78 : le `code` technique stable de Category.
|
||||
*
|
||||
* Cas couverts :
|
||||
* - POST : le code est auto-genere (slug MAJUSCULE du nom) et expose en lecture ;
|
||||
* - le code est en lecture seule : un `code` envoye dans le payload est ignore
|
||||
* (genere depuis le nom) ;
|
||||
* - deux noms produisant le meme slug recoivent des codes distincts (suffixe).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CategoryCodeTest extends AbstractCatalogApiTestCase
|
||||
{
|
||||
public function testPostGeneratesAndExposesCode(): void
|
||||
{
|
||||
$type = $this->createCategoryType();
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
$response = $client->request('POST', '/api/categories', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => self::TEST_CATEGORY_PREFIX.'Agro-alimentaire',
|
||||
'categoryType' => '/api/category_types/'.$type->getId(),
|
||||
],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
|
||||
$payload = $response->toArray();
|
||||
// Slug MAJUSCULE du nom, separateurs non alphanumeriques -> `_`.
|
||||
self::assertSame(
|
||||
strtoupper(self::TEST_CATEGORY_PREFIX).'AGRO_ALIMENTAIRE',
|
||||
$payload['code'],
|
||||
);
|
||||
}
|
||||
|
||||
public function testCodeIsReadOnlyAndIgnoredFromPayload(): void
|
||||
{
|
||||
$type = $this->createCategoryType();
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
$response = $client->request('POST', '/api/categories', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => self::TEST_CATEGORY_PREFIX.'readonly',
|
||||
'categoryType' => '/api/category_types/'.$type->getId(),
|
||||
// Le client tente d'imposer un code : doit etre ignore.
|
||||
'code' => 'CLIENT_FORGED',
|
||||
],
|
||||
]);
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
|
||||
$payload = $response->toArray();
|
||||
self::assertNotSame('CLIENT_FORGED', $payload['code']);
|
||||
self::assertSame(strtoupper(self::TEST_CATEGORY_PREFIX).'READONLY', $payload['code']);
|
||||
}
|
||||
|
||||
public function testCollidingSlugsGetDistinctCodes(): void
|
||||
{
|
||||
$type = $this->createCategoryType();
|
||||
$client = $this->createAdminClient();
|
||||
|
||||
// Deux noms differents (donc autorises par uq_category_name_type_active)
|
||||
// mais qui produisent le meme slug -> codes distincts (suffixe `_2`).
|
||||
$first = $client->request('POST', '/api/categories', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => self::TEST_CATEGORY_PREFIX.'Agro Plus',
|
||||
'categoryType' => '/api/category_types/'.$type->getId(),
|
||||
],
|
||||
])->toArray();
|
||||
|
||||
$second = $client->request('POST', '/api/categories', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => [
|
||||
'name' => self::TEST_CATEGORY_PREFIX.'Agro-Plus',
|
||||
'categoryType' => '/api/category_types/'.$type->getId(),
|
||||
],
|
||||
])->toArray();
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertNotSame($first['code'], $second['code']);
|
||||
self::assertStringEndsWith('_2', (string) $second['code']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user