264 lines
14 KiB
PHP
264 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
use App\Shared\Infrastructure\Database\ColumnCommentsCatalog;
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
use Doctrine\Migrations\AbstractMigration;
|
|
|
|
/**
|
|
* M6 — Catalogue produit (ERP-198) : creation du schema BDD du module.
|
|
*
|
|
* Objets crees (spec-back § 3.2) :
|
|
* - storage_type : referentiel PROVISOIRE des types de stockage (en attente de la
|
|
* liste definitive d'Aurore — § 2.4 / RG-6.06). Lecture seule au M6.
|
|
* - storage_type_site : jonction M2M storage_type <-> site (sur quels sites un type
|
|
* de stockage est disponible — alimente le filtrage du multi-select par site).
|
|
* - product : table principale (code unique global parmi les actifs, etats
|
|
* multi-valeur JSONB, champs conditionnels SALE, categorie de type PRODUIT,
|
|
* soft-delete prepare + Timestampable/Blamable).
|
|
* - product_site : jonction M2M product <-> site (sites de disponibilite, RG-6.04).
|
|
* - product_storage_type : jonction M2M product <-> storage_type (RG-6.06).
|
|
*
|
|
* Seed : ajout du `category_type` PRODUIT (miroir CategoryTypeFixtures, comme
|
|
* CLIENT/FOURNISSEUR/PRESTATAIRE/ADRESSE — § 2.5). Les `Category` de type PRODUIT et
|
|
* le seed Figma du referentiel storage_type suivent au ticket ERP-201.
|
|
*
|
|
* Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire :
|
|
* la table product porte des FK cross-module (user, site, category). Le tri par
|
|
* timestamp au sein du namespace racine garantit l'ordre apres la creation de ces
|
|
* tables sur base vide ; un namespace modulaire casserait `make db-reset` (cf.
|
|
* Version20260617150000 pour le M5).
|
|
*
|
|
* Convention IDs (spec § 2.2) : `INT GENERATED BY DEFAULT AS IDENTITY`,
|
|
* horodatages `TIMESTAMP(0) WITHOUT TIME ZONE` (le TimestampableBlamableTrait mappe
|
|
* `datetime_immutable`). Chaque colonne porte son `COMMENT ON COLUMN` (regle n°12).
|
|
*
|
|
* NB schema:update (test-db-setup) : product / storage_type et leurs jonctions seront
|
|
* mappes en ORM au ticket suivant (entites Product + StorageType, ERP-199). D'ici la,
|
|
* `schema:update --force` les drope sur la base de TEST uniquement (sans impact :
|
|
* aucun test ne les reference encore, et dev/prod ne lancent jamais schema:update).
|
|
* Leurs descriptions seront ajoutees a ColumnCommentsCatalog au ticket entites (comme
|
|
* weighing_ticket : migration ERP-182, catalogue ERP-183).
|
|
*/
|
|
final class Version20260625110000 extends AbstractMigration
|
|
{
|
|
public function getDescription(): string
|
|
{
|
|
return 'ERP-198 (M6) : storage_type (+ jonction site) + product (+ jonctions site/stockage) + seed category_type PRODUIT.';
|
|
}
|
|
|
|
public function up(Schema $schema): void
|
|
{
|
|
$this->createStorageType();
|
|
$this->createStorageTypeSite();
|
|
$this->createProduct();
|
|
$this->createProductSite();
|
|
$this->createProductStorageType();
|
|
$this->seedCategoryTypeProduit();
|
|
}
|
|
|
|
public function down(Schema $schema): void
|
|
{
|
|
// Ordre inverse des dependances FK.
|
|
$this->addSql('DROP TABLE IF EXISTS product_storage_type');
|
|
$this->addSql('DROP TABLE IF EXISTS product_site');
|
|
$this->addSql('DROP TABLE IF EXISTS product');
|
|
$this->addSql('DROP TABLE IF EXISTS storage_type_site');
|
|
$this->addSql('DROP TABLE IF EXISTS storage_type');
|
|
// Retrait du type seede (best-effort : echoue si des categories le referencent
|
|
// encore — attendu, le down sert au dev sur base saine).
|
|
$this->addSql("DELETE FROM category_type WHERE code = 'PRODUIT'");
|
|
}
|
|
|
|
// =================================================================
|
|
// Referentiel des types de stockage (PROVISOIRE) — § 2.4 / RG-6.06
|
|
// =================================================================
|
|
|
|
private function createStorageType(): void
|
|
{
|
|
$this->addSql(<<<'SQL'
|
|
CREATE TABLE storage_type (
|
|
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
|
code VARCHAR(40) NOT NULL,
|
|
label VARCHAR(120) NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
SQL);
|
|
|
|
$this->addSql('CREATE UNIQUE INDEX uq_storage_type_code ON storage_type (code)');
|
|
|
|
$this->comment('storage_type', '_table', 'Referentiel des types de stockage (PROVISOIRE, en attente liste Aurore) — Boisseau, Cellule, Tas, Cuve melasse… (RG-6.06). Lecture seule au M6.');
|
|
$this->comment('storage_type', 'id', 'Identifiant interne auto-incremente.');
|
|
$this->comment('storage_type', 'code', 'Code stable MAJUSCULE du type de stockage (ex. TAS, CUVE_MELASSE). Unique (uq_storage_type_code).');
|
|
$this->comment('storage_type', 'label', 'Libelle FR affiche du type de stockage (ex. « Cuve melasse »).');
|
|
}
|
|
|
|
private function createStorageTypeSite(): void
|
|
{
|
|
$this->addSql(<<<'SQL'
|
|
CREATE TABLE storage_type_site (
|
|
storage_type_id INT NOT NULL,
|
|
site_id INT NOT NULL,
|
|
PRIMARY KEY (storage_type_id, site_id),
|
|
CONSTRAINT fk_storage_type_site_type
|
|
FOREIGN KEY (storage_type_id) REFERENCES storage_type (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_storage_type_site_site
|
|
FOREIGN KEY (site_id) REFERENCES site (id) ON DELETE CASCADE
|
|
)
|
|
SQL);
|
|
|
|
$this->addSql('CREATE INDEX idx_storage_type_site_site ON storage_type_site (site_id)');
|
|
|
|
$this->comment('storage_type_site', '_table', 'Jointure M2M storage_type <-> site (Sites) — sites sur lesquels un type de stockage est disponible (alimente le filtrage du multi-select par site, RG-6.06).');
|
|
$this->comment('storage_type_site', 'storage_type_id', 'FK -> storage_type.id, ON DELETE CASCADE — type de stockage disponible.');
|
|
$this->comment('storage_type_site', 'site_id', 'FK -> site.id, ON DELETE CASCADE — site ou le type de stockage est disponible.');
|
|
}
|
|
|
|
// =================================================================
|
|
// Table principale `product`
|
|
// =================================================================
|
|
|
|
private function createProduct(): void
|
|
{
|
|
$this->addSql(<<<'SQL'
|
|
CREATE TABLE product (
|
|
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
|
code VARCHAR(50) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
states JSONB DEFAULT '[]'::jsonb NOT NULL,
|
|
manufactured BOOLEAN DEFAULT FALSE NOT NULL,
|
|
contains_molasses BOOLEAN DEFAULT FALSE NOT NULL,
|
|
category_id INT NOT NULL,
|
|
deleted_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
|
|
created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
|
updated_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
|
|
created_by INT DEFAULT NULL,
|
|
updated_by INT DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
CONSTRAINT chk_product_states_not_empty
|
|
CHECK (jsonb_array_length(states) >= 1),
|
|
CONSTRAINT fk_product_category
|
|
FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT,
|
|
CONSTRAINT fk_product_created_by
|
|
FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL,
|
|
CONSTRAINT fk_product_updated_by
|
|
FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL
|
|
)
|
|
SQL);
|
|
|
|
// Unicite GLOBALE du code parmi les actifs (soft-delete tolere) — index partiel.
|
|
$this->addSql('CREATE UNIQUE INDEX uq_product_code_active ON product (code) WHERE deleted_at IS NULL');
|
|
$this->addSql('CREATE INDEX idx_product_category ON product (category_id)');
|
|
$this->addSql('CREATE INDEX idx_product_deleted_at ON product (deleted_at)');
|
|
$this->addSql('CREATE INDEX idx_product_created_by ON product (created_by)');
|
|
$this->addSql('CREATE INDEX idx_product_updated_by ON product (updated_by)');
|
|
|
|
$this->comment('product', '_table', 'Produits du catalogue (M6 Catalog) — etat Achat/Vendu/Autre, sites de disponibilite, categorie produit, types de stockage.');
|
|
$this->comment('product', 'id', 'Identifiant interne auto-incremente.');
|
|
$this->comment('product', 'code', 'Code produit (= « Numero » de la liste), saisi, unique global parmi les actifs (RG-6.01). Index partiel uq_product_code_active. Normalise serveur (trim/UPPER).');
|
|
$this->comment('product', 'name', 'Nom du produit (≤ 255). Normalise serveur (trim).');
|
|
$this->comment('product', 'states', 'Etats du produit (JSON) : sous-ensemble non vide de PURCHASE|SALE|OTHER, multi-select (RG-6.02, chk_product_states_not_empty). Pilote les champs conditionnels.');
|
|
$this->comment('product', 'manufactured', '« Fabrique » : saisi uniquement si states contient SALE, sinon force false serveur (RG-6.03).');
|
|
$this->comment('product', 'contains_molasses', '« Contient de la melasse » : saisi uniquement si states contient SALE, sinon force false serveur (RG-6.03).');
|
|
$this->comment('product', 'category_id', 'Categorie produit (FK -> category.id, ON DELETE RESTRICT) — type PRODUIT, obligatoire, validee applicativement (RG-6.05).');
|
|
$this->comment('product', 'deleted_at', 'Horodatage du soft-delete technique — non expose au M6 ; la liste exclut les produits supprimes (§ 2.7). Null = ligne active.');
|
|
$this->addTimestampableBlamableComments('product');
|
|
}
|
|
|
|
private function createProductSite(): void
|
|
{
|
|
$this->addSql(<<<'SQL'
|
|
CREATE TABLE product_site (
|
|
product_id INT NOT NULL,
|
|
site_id INT NOT NULL,
|
|
PRIMARY KEY (product_id, site_id),
|
|
CONSTRAINT fk_product_site_product
|
|
FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_product_site_site
|
|
FOREIGN KEY (site_id) REFERENCES site (id) ON DELETE RESTRICT
|
|
)
|
|
SQL);
|
|
|
|
$this->addSql('CREATE INDEX idx_product_site_site ON product_site (site_id)');
|
|
|
|
$this->comment('product_site', '_table', 'Jointure M2M product <-> site (Sites) — sites de disponibilite du produit (>= 1 obligatoire, RG-6.04).');
|
|
$this->comment('product_site', 'product_id', 'FK -> product.id, ON DELETE CASCADE — produit concerne.');
|
|
$this->comment('product_site', 'site_id', 'FK -> site.id, ON DELETE RESTRICT — site de disponibilite rattache au produit.');
|
|
}
|
|
|
|
private function createProductStorageType(): void
|
|
{
|
|
$this->addSql(<<<'SQL'
|
|
CREATE TABLE product_storage_type (
|
|
product_id INT NOT NULL,
|
|
storage_type_id INT NOT NULL,
|
|
PRIMARY KEY (product_id, storage_type_id),
|
|
CONSTRAINT fk_product_storage_type_product
|
|
FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_product_storage_type_type
|
|
FOREIGN KEY (storage_type_id) REFERENCES storage_type (id) ON DELETE RESTRICT
|
|
)
|
|
SQL);
|
|
|
|
$this->addSql('CREATE INDEX idx_product_storage_type_type ON product_storage_type (storage_type_id)');
|
|
|
|
$this->comment('product_storage_type', '_table', 'Jointure M2M product <-> storage_type — types de stockage du produit (>= 1 obligatoire, filtres par les sites selectionnes, RG-6.06).');
|
|
$this->comment('product_storage_type', 'product_id', 'FK -> product.id, ON DELETE CASCADE — produit concerne.');
|
|
$this->comment('product_storage_type', 'storage_type_id', 'FK -> storage_type.id, ON DELETE RESTRICT — type de stockage rattache au produit.');
|
|
}
|
|
|
|
// =================================================================
|
|
// Seed du type de categorie PRODUIT (§ 2.5) — miroir CategoryTypeFixtures
|
|
// =================================================================
|
|
|
|
private function seedCategoryTypeProduit(): void
|
|
{
|
|
// Idempotent via l'index unique uq_category_type_code (comme CLIENT/FOURNISSEUR/
|
|
// PRESTATAIRE/ADRESSE). Les Category de type PRODUIT suivent en ERP-201.
|
|
$this->addSql(<<<'SQL'
|
|
INSERT INTO category_type (code, label) VALUES ('PRODUIT', 'Produit')
|
|
ON CONFLICT (code) DO NOTHING
|
|
SQL);
|
|
}
|
|
|
|
// =================================================================
|
|
// Helpers (identiques au M5 Version20260617150000)
|
|
// =================================================================
|
|
|
|
/**
|
|
* Pose les 4 commentaires standardises Timestampable/Blamable sur une table,
|
|
* en reutilisant le catalogue partage (source unique, ERP-67).
|
|
*/
|
|
private function addTimestampableBlamableComments(string $table): void
|
|
{
|
|
foreach (ColumnCommentsCatalog::timestampableBlamableComments() as $column => $description) {
|
|
$this->comment($table, $column, $description);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emet un `COMMENT ON TABLE` (colonne speciale `_table`) ou `COMMENT ON COLUMN`
|
|
* en dollar-quoting Postgres ($_$...$_$) pour eviter tout echappement d apostrophe.
|
|
*/
|
|
private function comment(string $table, string $column, string $description): void
|
|
{
|
|
$quotedTable = '"'.str_replace('"', '""', $table).'"';
|
|
|
|
if ('_table' === $column) {
|
|
$this->addSql(sprintf('COMMENT ON TABLE %s IS $_$%s$_$', $quotedTable, $description));
|
|
|
|
return;
|
|
}
|
|
|
|
$this->addSql(sprintf(
|
|
'COMMENT ON COLUMN %s.%s IS $_$%s$_$',
|
|
$quotedTable,
|
|
'"'.str_replace('"', '""', $column).'"',
|
|
$description,
|
|
));
|
|
}
|
|
}
|