d97b9ce6d0
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
122 lines
5.6 KiB
PHP
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);
|
|
}
|
|
}
|