feat(technique) : module Technique + taxonomie categories prestataires
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m13s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m27s

Cree le nouveau module Technique (pole distinct du Commercial) prerequis du
M3 repertoire prestataires :
- TechniqueModule (ID=technique, REQUIRED=false) + 5 permissions RBAC
  technique.providers.* (view / manage / accounting.view / accounting.manage
  / archive), declarees pour app:sync-permissions.
- Activation dans config/modules.php + layer front frontend/modules/technique/.
- Seed taxonomie : nouveau CategoryType PRESTATAIRE + 3 categories
  (Maintenance industrielle, Nettoyage, Transport) via migration idempotente
  (ON CONFLICT / NOT EXISTS, jonction M2M category_category_type) ET fixtures
  CategoryType/Category (survivent au purger db-reset).
- Tests : structure du module (5 permissions figees) + filtre
  GET /api/categories?typeCode=PRESTATAIRE.

Inclut la spec back/front M3 et le RETEX M1.
This commit is contained in:
Matthieu
2026-06-12 09:23:08 +02:00
parent 7d8a633eee
commit 6ceef62056
11 changed files with 1795 additions and 3 deletions
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace App\Tests\Module\Catalog\Api;
use App\Module\Catalog\Domain\Entity\CategoryType;
/**
* Tests du seed de la taxonomie PRESTATAIRE (M3 1.1) cote API.
*
* Le multi-select « Categorie » du prestataire (formulaire principal + adresse)
* consomme `GET /api/categories?typeCode=PRESTATAIRE`. Ce test prouve que :
* - le filtre `?typeCode=PRESTATAIRE` ne renvoie QUE les categories du type
* PRESTATAIRE (aucune fuite de categorie d'un autre type) ;
* - chaque membre renvoye porte bien le type PRESTATAIRE dans `categoryTypes`.
*
* NB : la base de test est purgee de toute categorie / type entre chaque test
* (cf. AbstractCatalogApiTestCase::cleanupCatalogTestData), donc le type et les
* categories PRESTATAIRE sont materialises ici (et non lus depuis le seed de la
* migration / fixture, qui ne survit pas a la purge). On valide ainsi le contrat
* du filtre sur le code reel `PRESTATAIRE`. La presence du seed apres un
* `make db-reset` reel est, elle, verifiee par l'idempotence des fixtures.
*
* @internal
*/
final class CategoryPrestataireSeedTest extends AbstractCatalogApiTestCase
{
/**
* Categories de demonstration seedees par la migration / fixture PRESTATAIRE.
*/
private const array PROVIDER_CATEGORIES = [
'Maintenance industrielle',
'Nettoyage',
'Transport',
];
public function testTypeCodePrestataireReturnsOnlyProviderCategories(): void
{
$providerType = $this->getOrCreatePrestataireType();
foreach (self::PROVIDER_CATEGORIES as $name) {
$this->createCategory($name, $providerType);
}
// Bruit : un type + une categorie d'un autre type ne doivent PAS fuiter.
$noiseType = $this->createCategoryType('TEST_FOURNISSEUR', 'Test Fournisseur');
$this->createCategory(self::TEST_CATEGORY_PREFIX.'noise', $noiseType);
$client = $this->createAdminClient();
$response = $client->request('GET', '/api/categories?typeCode=PRESTATAIRE&pagination=false');
self::assertSame(200, $response->getStatusCode());
$members = $response->toArray()['member'];
$names = array_map(static fn (array $m): string => $m['name'], $members);
sort($names);
$expected = self::PROVIDER_CATEGORIES;
sort($expected);
self::assertSame(
$expected,
$names,
'Le filtre ?typeCode=PRESTATAIRE doit ne renvoyer QUE les categories du type PRESTATAIRE.',
);
// Chaque categorie remontee doit PORTER le type PRESTATAIRE.
foreach ($members as $member) {
self::assertContains('PRESTATAIRE', array_column($member['categoryTypes'], 'code'));
}
}
public function testTypeCodePrestataireKeepsHydraPagination(): void
{
$providerType = $this->getOrCreatePrestataireType();
$this->createCategory('Maintenance industrielle', $providerType);
$client = $this->createAdminClient();
$response = $client->request('GET', '/api/categories?typeCode=PRESTATAIRE');
self::assertSame(200, $response->getStatusCode());
$data = $response->toArray();
self::assertArrayHasKey('totalItems', $data, 'Le filtre ne doit pas casser la pagination Hydra.');
self::assertArrayHasKey('member', $data);
foreach ($data['member'] as $member) {
self::assertContains('PRESTATAIRE', array_column($member['categoryTypes'], 'code'));
}
}
/**
* Recupere le type PRESTATAIRE reel, ou le cree s'il est absent. Le code
* `PRESTATAIRE` est seede par CategoryTypeFixtures (present en debut de suite),
* mais le cleanup purge tous les `category_type` entre les tests : selon
* l'ordre d'execution, le type peut donc exister ou non. Le get-or-create rend
* le test robuste sans dependre du seed ni le dupliquer.
*/
private function getOrCreatePrestataireType(): CategoryType
{
$em = $this->getEm();
$existing = $em->getRepository(CategoryType::class)->findOneBy(['code' => 'PRESTATAIRE']);
if ($existing instanceof CategoryType) {
return $existing;
}
return $this->createCategoryType('PRESTATAIRE', 'Prestataire');
}
}
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Tests\Module\Technique;
use App\Module\Technique\TechniqueModule;
use PHPUnit\Framework\TestCase;
/**
* Tests structurels du module Technique (M3) : identite et contrat
* `permissions()`.
*
* @internal
*/
final class TechniqueModuleTest extends TestCase
{
public function testModuleIdentity(): void
{
self::assertSame('technique', TechniqueModule::ID);
self::assertSame('Technique', TechniqueModule::LABEL);
self::assertFalse(TechniqueModule::REQUIRED);
}
public function testPermissionsSetContainsExactlyFiveCodes(): void
{
// Garde-fou : le jeu de permissions du module est fige par ce test. Si
// quelqu'un ajoute / retire une permission sans ajuster la spec (§ 5.1)
// ni la matrice RBAC, le test casse explicitement.
$codes = array_column(TechniqueModule::permissions(), 'code');
sort($codes);
self::assertSame(
[
'technique.providers.accounting.manage',
'technique.providers.accounting.view',
'technique.providers.archive',
'technique.providers.manage',
'technique.providers.view',
],
$codes,
);
}
public function testEveryPermissionCodeIsPrefixedByModuleId(): void
{
// Convention de nommage `module.resource[.sub].action` : le prefixe doit
// correspondre exactement a l'ID du module (verifie aussi par
// app:sync-permissions).
foreach (TechniqueModule::permissions() as $permission) {
self::assertStringStartsWith(
TechniqueModule::ID.'.',
$permission['code'],
'Chaque code de permission doit etre prefixe par l\'ID du module.',
);
self::assertNotSame('', $permission['label'], 'Chaque permission doit porter un label.');
}
}
}