Files
Starseed/migrations/Version20260612080000.php
T
matthieu d97b9ce6d0
Auto Tag Develop / tag (push) Successful in 11s
feat(technique) : module Technique + taxonomie categories prestataires (#89)
## 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
2026-06-12 14:19:14 +00:00

122 lines
5.6 KiB
PHP

<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* M3 (ticket 1.1) — Taxonomie PRESTATAIRE (module Catalog, prerequis du module Technique).
*
* Contexte : le M3 (repertoire prestataires) a besoin d'une taxonomie distincte
* des types CLIENT (M1) et FOURNISSEUR (M2). Decision Matthieu (11/06) : types
* distincts CLIENT / FOURNISSEUR / PRESTATAIRE, chacun avec sa taxonomie. Le
* multi-select « Categorie » du prestataire (formulaire principal + adresse)
* ne reference que des `Category` rattachees au type PRESTATAIRE (RG-3.09).
*
* Cette migration :
* 1. cree le `category_type` PRESTATAIRE (code PRESTATAIRE, label « Prestataire ») ;
* 2. seede 3 `Category` de demonstration rattachees a ce type via la jonction
* ManyToMany `category_category_type` (modele courant depuis Version20260608120000 ;
* la colonne ManyToOne `category.category_type_id` n'existe plus).
*
* Aucune colonne creee/modifiee -> pas de `COMMENT ON COLUMN` (regle ABSOLUE n°12) :
* la migration ne fait que des INSERT de donnees de reference.
*
* Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire :
* avec plusieurs migrations_paths, Doctrine Migrations 3.x trie par FQCN
* alphabetique -> une migration `App\Module\...` passerait avant les
* `DoctrineMigrations\...` sur base vide, donc avant la creation des tables
* `category` / `category_type` / `category_category_type`. Le namespace racine
* garantit l'ordre par timestamp.
*
* Idempotence : `INSERT ... ON CONFLICT (code) DO NOTHING` pour le type,
* `INSERT ... SELECT ... WHERE NOT EXISTS` pour chaque categorie et chaque ligne
* de jonction (aligne sur le pattern ERP-84 / Version20260605120000). En prod la
* table `category` est vide (aucune fixture metier). En dev/test, le purger
* Doctrine vide `category` / `category_type` avant les fixtures qui reproduisent
* le meme etat final (CategoryTypeFixtures / CategoryFixtures etendus a PRESTATAIRE).
*/
final class Version20260612080000 extends AbstractMigration
{
/**
* Categories de demonstration du type PRESTATAIRE : nom => code stable. Le
* code est la cle metier (slug MAJUSCULE du nom, miroir du
* CategoryCodeGenerator) et reste unique parmi les actifs (uq_category_code,
* partage avec les codes CLIENT / FOURNISSEUR — aucune collision ici). Le nom
* est unique GLOBALEMENT parmi les actifs (uq_category_name_active) : les
* libelles ci-dessous n'entrent en collision avec aucune categorie seedee.
*/
private const array PROVIDER_CATEGORIES = [
'Maintenance industrielle' => 'MAINTENANCE_INDUSTRIELLE',
'Nettoyage' => 'NETTOYAGE',
'Transport' => 'TRANSPORT',
];
public function getDescription(): string
{
return 'M3 1.1 : cree le CategoryType PRESTATAIRE + seed des categories prestataires (Maintenance industrielle, Nettoyage, Transport).';
}
public function up(Schema $schema): void
{
// 1. Type PRESTATAIRE (idempotent via l'index unique uq_category_type_code).
$this->addSql(<<<'SQL'
INSERT INTO category_type (code, label) VALUES ('PRESTATAIRE', 'Prestataire')
ON CONFLICT (code) DO NOTHING
SQL);
foreach (self::PROVIDER_CATEGORIES as $name => $code) {
// 2a. Categorie sous PRESTATAIRE (si le code est libre parmi les
// actifs). created_at/updated_at NOT NULL -> NOW() ; le blame
// reste null (seed hors contexte HTTP, libelle « Systeme » cote front).
$this->addSql(<<<'SQL'
INSERT INTO category (name, code, created_at, updated_at)
SELECT :name, :code, NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM category c WHERE c.code = :code AND c.deleted_at IS NULL
)
SQL, ['name' => $name, 'code' => $code]);
// 2b. Jonction M2M categorie <-> type PRESTATAIRE (modele courant).
$this->addSql(<<<'SQL'
INSERT INTO category_category_type (category_id, category_type_id)
SELECT c.id, ct.id
FROM category c
CROSS JOIN category_type ct
WHERE c.code = :code AND c.deleted_at IS NULL
AND ct.code = 'PRESTATAIRE'
AND NOT EXISTS (
SELECT 1 FROM category_category_type cct
WHERE cct.category_id = c.id AND cct.category_type_id = ct.id
)
SQL, ['code' => $code]);
}
}
public function down(Schema $schema): void
{
// Best-effort : on retire d'abord les categories seedees (par code) — la FK
// category_category_type est ON DELETE CASCADE cote category, donc les
// lignes de jonction partent avec —, puis le type s'il n'est plus reference.
$this->addSql(
'DELETE FROM category WHERE code IN (:codes) '
."AND id IN (SELECT category_id FROM category_category_type cct "
."JOIN category_type ct ON ct.id = cct.category_type_id WHERE ct.code = 'PRESTATAIRE')",
['codes' => array_values(self::PROVIDER_CATEGORIES)],
['codes' => ArrayParameterType::STRING],
);
$this->addSql(<<<'SQL'
DELETE FROM category_type
WHERE code = 'PRESTATAIRE'
AND NOT EXISTS (
SELECT 1 FROM category_category_type cct WHERE cct.category_type_id = category_type.id
)
SQL);
}
}