feat(technique) : module Technique + taxonomie categories prestataires (#89)
Auto Tag Develop / tag (push) Successful in 11s

## M3 — Ticket 1.1 : module Technique + taxonomie catégories prestataires

Prérequis de tout le M3 (répertoire prestataires). Spec : `docs/specs/M3-prestataires/spec-back.md` § 2.1 + § 2.4.

### Contenu
- **Nouveau module `Technique`** (`src/Module/Technique/TechniqueModule.php`) : `ID=technique`, `LABEL=Technique`, `REQUIRED=false`, `permissions()` (5 codes `technique.providers.*` : view / manage / accounting.view / accounting.manage / archive).
- Activation dans `config/modules.php` → `/api/modules` expose `technique`.
- **Layer front** `frontend/modules/technique/` (auto-détecté).
- **Seed taxonomie PRESTATAIRE** : nouveau `CategoryType` (code `PRESTATAIRE` / label `Prestataire`) + 3 catégories (Maintenance industrielle, Nettoyage, Transport).
  - Migration racine idempotente `Version20260612080000` (`ON CONFLICT DO NOTHING` + `NOT EXISTS`, jonction M2M `category_category_type` — schéma courant, pas l'ancien `category_type_id`).
  - Fixtures `CategoryTypeFixtures` / `CategoryFixtures` étendues (survivent au purger `db-reset`).

### Critères d'acceptation 
- [x] Module + permissions déclarées (`app:sync-permissions` → 5 codes en base)
- [x] `TechniqueModule::class` dans `config/modules.php`
- [x] Layer front
- [x] Seed CategoryType PRESTATAIRE (migration + fixture idempotente)
- [x] ≥ 3 catégories PRESTATAIRE
- [x] `GET /api/categories?typeCode=PRESTATAIRE` filtre correctement

### Tests
- `TechniqueModuleTest` : identité + jeu de 5 permissions figé.
- `CategoryPrestataireSeedTest` : `?typeCode=PRESTATAIRE` ne renvoie QUE le type PRESTATAIRE + pagination Hydra préservée.
- `make test` : **589 tests OK** · `php-cs-fixer` : 0 correction · `make db-reset` : type + 3 catégories présents, idempotent.

### Hors-périmètre (tickets M3 suivants)
Section sidebar « Technique », personas RBAC E2E, et entités `Provider*` (l'écran `/providers` n'existe pas encore → pas de lien mort introduit ici).

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #89
This commit was merged in pull request #89.
This commit is contained in:
2026-06-12 14:19:14 +00:00
parent b36520d3b1
commit d97b9ce6d0
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');
}
}