diff --git a/migrations/Version20260626110000.php b/migrations/Version20260626110000.php new file mode 100644 index 0000000..f3b29b4 --- /dev/null +++ b/migrations/Version20260626110000.php @@ -0,0 +1,101 @@ + 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) : depend de + * `category` / `category_type` / `category_category_type` (creees au namespace racine) + * et du type PRODUIT (Version20260625110000) ; le tri par timestamp garantit l'ordre. + * + * Idempotence : `INSERT ... SELECT ... WHERE NOT EXISTS` pour chaque categorie et + * chaque ligne de jonction (miroir Version20260612080000 / ERP-84). Codes = slug + * MAJUSCULE deterministe (meme sortie que CategoryCodeGenerator), provisoires — a + * affiner avec le metier (ERP-201). + */ +final class Version20260626110000 extends AbstractMigration +{ + /** + * Categories produit (provisoires, Figma/metier) : nom => code stable. Le code + * reste unique parmi les actifs (uq_category_code) et le nom unique globalement + * (uq_category_name_active) — aucune collision avec les taxonomies existantes. + */ + private const array PRODUCT_CATEGORIES = [ + 'Céréales' => 'CEREALES', + 'Oléagineux' => 'OLEAGINEUX', + 'Aliments du bétail' => 'ALIMENTS_DU_BETAIL', + 'Engrais' => 'ENGRAIS', + ]; + + public function getDescription(): string + { + return 'M6 Catalog : seed prod-safe des categories de type PRODUIT (Cereales, Oleagineux, Aliments du betail, Engrais).'; + } + + public function up(Schema $schema): void + { + // Le type PRODUIT existe deja (Version20260625110000) ; re-assert defensif + // et idempotent pour rendre cette migration auto-portante. + $this->addSql(<<<'SQL' + INSERT INTO category_type (code, label) VALUES ('PRODUIT', 'Produit') + ON CONFLICT (code) DO NOTHING + SQL); + + foreach (self::PRODUCT_CATEGORIES as $name => $code) { + // 1. Categorie (si le code est libre parmi les actifs). created_at/updated_at + // NOT NULL -> NOW() ; le blame reste null (seed hors contexte HTTP). + $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]); + + // 2. Jonction M2M categorie <-> type PRODUIT (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 = 'PRODUIT' + 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 : retire les categories seedees (par code) rattachees au type + // PRODUIT — la jonction part en CASCADE cote category. Echoue si un produit + // reference encore l'une d'elles (FK RESTRICT product.category_id), attendu. + $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 = 'PRODUIT')", + ['codes' => array_values(self::PRODUCT_CATEGORIES)], + ['codes' => ArrayParameterType::STRING], + ); + } +}