From 1a29bcf76c49ba349cf8a130b5a86130e378a455 Mon Sep 17 00:00:00 2001 From: THOLOT DECHENE Matthieu Date: Mon, 8 Jun 2026 07:06:01 +0000 Subject: [PATCH] feat(commercial) : migration BDD M2 fournisseurs (supplier + sous-collections + M2M) (ERP-85) (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ERP-85 — Migration BDD M2 Fournisseurs (étape 1/7) PR **empilée sur ERP-84** (#63) : ne contient que le commit ERP-85. À merger après #63 (la base rebascule sur develop automatiquement au merge de #63). ### Contenu Migration `Version20260605130000.php` (namespace racine `DoctrineMigrations`) — schéma M2 sous le module Commercial, jumeau du M1 client. **8 tables** : `supplier`, `supplier_category` (M2M), `supplier_contact`, `supplier_address`, `supplier_address_site` / `_contact` / `_category` (3 M2M), `supplier_rib`. **Spécificités M2 (vs M1 client)** - `supplier` **sans contact inline** (ERP-106) ni auto-référence distributor/broker ; ajout `volume_forecast`. - `supplier_address` : enum `address_type` `CHECK (PROSPECT|DEPART|RENDU)`, `bennes` + `triage_provider`, **pas** de `billing_email`. - Index partiel unique `uq_supplier_company_name_active` (nom seul, hors archives/soft-delete). **Réutilisations (zéro duplication)** : référentiels comptables M1 (`tva_mode`/`payment_delay`/`payment_type`/`bank`) + `CategoryType FOURNISSEUR` (seedé par ERP-84). Pas de re-seed. **Conventions** : `COMMENT ON COLUMN` sur chaque colonne (règle n°12) + helper Timestampable/Blamable ; namespace racine (FK cross-module, exception règle n°11). ### Vérifications - `make db-reset` ✅ de bout en bout (aucune erreur FK) - `make test` ✅ 483 tests OK (`ColumnsHaveSqlCommentTest` vert, 0 colonne sans commentaire) - `make php-cs-fixer-allow-risky` ✅ 0 fichier à corriger Bloque : #86 (entités `Supplier*` + ApiResource). --------- Co-authored-by: admin malio Co-authored-by: Matthieu Reviewed-on: https://gitea.malio.fr/MALIO-DEV/Starseed/pulls/64 Co-authored-by: THOLOT DECHENE Matthieu Co-committed-by: THOLOT DECHENE Matthieu --- migrations/Version20260605130000.php | 438 +++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 migrations/Version20260605130000.php diff --git a/migrations/Version20260605130000.php b/migrations/Version20260605130000.php new file mode 100644 index 0000000..de1a1c1 --- /dev/null +++ b/migrations/Version20260605130000.php @@ -0,0 +1,438 @@ + echec des FK. Le namespace racine garantit l'ordre par timestamp. + * + * Style DDL aligne sur le M1 (Version20260601000000) : `INT GENERATED BY DEFAULT + * AS IDENTITY` (et non SERIAL), `TIMESTAMP(0) WITHOUT TIME ZONE` (et non + * TIMESTAMPTZ, car le TimestampableBlamableTrait mappe `datetime_immutable`). + * Garantit que `schema:update` restera un no-op quand les entites arriveront + * (ticket ERP-86). + * + * Decision unicite (Matthieu 02/06, alignee Q4 du M1) : unicite metier sur le + * NOM DE SOCIETE uniquement (uq_supplier_company_name_active, partiel). Pas + * d'index unique sur siren ni email. + * + * Chaque colonne porte un `COMMENT ON COLUMN` (regle ABSOLUE n°12, garde-fou + * ColumnsHaveSqlCommentTest). Les tables n'etant pas encore mappees par l'ORM + * (entites a ERP-86), ces commentaires survivent au `schema:update --force` du + * setup de test (additif, ne drop pas les tables non mappees). + */ +final class Version20260605130000 extends AbstractMigration +{ + public function getDescription(): string + { + return 'ERP-85 (M2) : tables supplier + sous-collections + jointures M2M (referentiels comptables et CategoryType FOURNISSEUR reutilises).'; + } + + public function up(Schema $schema): void + { + $this->createSupplierTable(); + $this->createSupplierCategory(); + $this->createSupplierContact(); + $this->createSupplierAddress(); + $this->createSupplierAddressJoinTables(); + $this->createSupplierRib(); + } + + public function down(Schema $schema): void + { + // Ordre inverse des dependances FK : jointures et sous-collections + // d'abord, puis supplier. Les referentiels comptables et le + // CategoryType FOURNISSEUR ne sont pas touches (crees ailleurs). + $this->addSql('DROP TABLE supplier_address_category'); + $this->addSql('DROP TABLE supplier_address_contact'); + $this->addSql('DROP TABLE supplier_address_site'); + $this->addSql('DROP TABLE supplier_rib'); + $this->addSql('DROP TABLE supplier_address'); + $this->addSql('DROP TABLE supplier_contact'); + $this->addSql('DROP TABLE supplier_category'); + $this->addSql('DROP TABLE supplier'); + } + + // ================================================================= + // Table principale `supplier` + // ================================================================= + + private function createSupplierTable(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE supplier ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + company_name VARCHAR(180) NOT NULL, + description TEXT DEFAULT NULL, + competitors VARCHAR(255) DEFAULT NULL, + founded_at DATE DEFAULT NULL, + employees_count INT DEFAULT NULL, + revenue_amount NUMERIC(15, 2) DEFAULT NULL, + director_name VARCHAR(120) DEFAULT NULL, + profit_amount NUMERIC(15, 2) DEFAULT NULL, + volume_forecast INT DEFAULT NULL, + siren VARCHAR(20) DEFAULT NULL, + account_number VARCHAR(40) DEFAULT NULL, + tva_mode_id INT DEFAULT NULL, + n_tva VARCHAR(40) DEFAULT NULL, + payment_delay_id INT DEFAULT NULL, + payment_type_id INT DEFAULT NULL, + bank_id INT DEFAULT NULL, + is_archived BOOLEAN DEFAULT FALSE NOT NULL, + archived_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT 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 fk_supplier_tva_mode + FOREIGN KEY (tva_mode_id) REFERENCES tva_mode (id) ON DELETE RESTRICT, + CONSTRAINT fk_supplier_payment_delay + FOREIGN KEY (payment_delay_id) REFERENCES payment_delay (id) ON DELETE RESTRICT, + CONSTRAINT fk_supplier_payment_type + FOREIGN KEY (payment_type_id) REFERENCES payment_type (id) ON DELETE RESTRICT, + CONSTRAINT fk_supplier_bank + FOREIGN KEY (bank_id) REFERENCES bank (id) ON DELETE RESTRICT, + CONSTRAINT fk_supplier_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_supplier_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + + $this->addSql('CREATE INDEX idx_supplier_is_archived ON supplier (is_archived)'); + $this->addSql('CREATE INDEX idx_supplier_deleted_at ON supplier (deleted_at)'); + $this->addSql('CREATE INDEX idx_supplier_created_by ON supplier (created_by)'); + $this->addSql('CREATE INDEX idx_supplier_updated_by ON supplier (updated_by)'); + + // Index sur les FK des referentiels comptables (Postgres n'indexe pas + // automatiquement les colonnes portant une FOREIGN KEY). + $this->addSql('CREATE INDEX idx_supplier_tva_mode_id ON supplier (tva_mode_id)'); + $this->addSql('CREATE INDEX idx_supplier_payment_delay_id ON supplier (payment_delay_id)'); + $this->addSql('CREATE INDEX idx_supplier_payment_type_id ON supplier (payment_type_id)'); + $this->addSql('CREATE INDEX idx_supplier_bank_id ON supplier (bank_id)'); + + // Unicite metier partielle : nom de societe insensible a la casse, parmi + // les non-archives ET non soft-deletes uniquement (spec § 2.6). Pas + // d'index unique sur siren ni email. + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX uq_supplier_company_name_active + ON supplier (LOWER(company_name)) + WHERE is_archived = FALSE AND deleted_at IS NULL + SQL); + + $this->comment('supplier', '_table', 'Repertoire fournisseurs (M2 Commercial) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M3).'); + $this->comment('supplier', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('supplier', 'company_name', 'Raison sociale du fournisseur (stockee en MAJUSCULES). Unique case-insensitive parmi les actifs non archives/non supprimes (uq_supplier_company_name_active, § 2.6).'); + $this->comment('supplier', 'description', 'Onglet Information : description libre. Obligatoire pour le role Commerciale (RG-2.03), optionnel sinon.'); + $this->comment('supplier', 'competitors', 'Onglet Information : concurrents identifies (texte libre ≤ 255). Obligatoire role Commerciale (RG-2.03).'); + $this->comment('supplier', 'founded_at', 'Onglet Information : date de creation de l entreprise. Obligatoire role Commerciale (RG-2.03).'); + $this->comment('supplier', 'employees_count', 'Onglet Information : effectif (entier >= 0). Obligatoire role Commerciale (RG-2.03).'); + $this->comment('supplier', 'revenue_amount', 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Obligatoire role Commerciale (RG-2.03).'); + $this->comment('supplier', 'director_name', 'Onglet Information : nom du dirigeant. Obligatoire role Commerciale (RG-2.03).'); + $this->comment('supplier', 'profit_amount', 'Onglet Information : resultat / benefice (NUMERIC 15,2). Obligatoire role Commerciale (RG-2.03).'); + $this->comment('supplier', 'volume_forecast', 'Onglet Information : volume previsionnel (entier >= 0) — specifique fournisseur. Obligatoire role Commerciale (RG-2.03).'); + $this->comment('supplier', 'siren', 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (§ 2.6).'); + $this->comment('supplier', 'account_number', 'Onglet Comptabilite : numero de compte comptable du fournisseur.'); + $this->comment('supplier', 'tva_mode_id', 'Onglet Comptabilite : mode de TVA applique — FK -> tva_mode.id (referentiel partage M1), ON DELETE RESTRICT.'); + $this->comment('supplier', 'n_tva', 'Onglet Comptabilite : numero de TVA intracommunautaire.'); + $this->comment('supplier', 'payment_delay_id', 'Onglet Comptabilite : delai de reglement — FK -> payment_delay.id (M1), ON DELETE RESTRICT.'); + $this->comment('supplier', 'payment_type_id', 'Onglet Comptabilite : type de reglement — FK -> payment_type.id (M1), ON DELETE RESTRICT. Pilote RG-2.07 (Banque si VIREMENT) et RG-2.08 (RIB).'); + $this->comment('supplier', 'bank_id', 'Onglet Comptabilite : banque — FK -> bank.id (M1), ON DELETE RESTRICT. Obligatoire ssi payment_type = VIREMENT (RG-2.07), null sinon.'); + $this->comment('supplier', 'is_archived', 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission commercial.suppliers.archive.'); + $this->comment('supplier', 'archived_at', 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration.'); + $this->comment('supplier', 'deleted_at', 'Horodatage du soft-delete technique (HP M3) — non expose par l API au M2. Null = ligne active.'); + $this->addTimestampableBlamableComments('supplier'); + } + + // ================================================================= + // M2M supplier <-> category (type FOURNISSEUR — RG-2.10) + // ================================================================= + + private function createSupplierCategory(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE supplier_category ( + supplier_id INT NOT NULL, + category_id INT NOT NULL, + PRIMARY KEY (supplier_id, category_id), + CONSTRAINT fk_supplier_category_supplier + FOREIGN KEY (supplier_id) REFERENCES supplier (id) ON DELETE CASCADE, + CONSTRAINT fk_supplier_category_category + FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT + ) + SQL); + $this->addSql('CREATE INDEX idx_supplier_category_category ON supplier_category (category_id)'); + + $this->comment('supplier_category', '_table', 'Jointure M2M supplier <-> category (Catalog) — categories de type FOURNISSEUR du fournisseur, au moins une obligatoire (RG-2.10).'); + $this->comment('supplier_category', 'supplier_id', 'FK -> supplier.id, ON DELETE CASCADE — fournisseur porteur de la categorie.'); + $this->comment('supplier_category', 'category_id', 'FK -> category.id, ON DELETE RESTRICT — categorie de type FOURNISSEUR rattachee au fournisseur (RG-2.10).'); + } + + // ================================================================= + // Sous-collection : contacts (1:n) + // ================================================================= + + private function createSupplierContact(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE supplier_contact ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + supplier_id INT NOT NULL, + first_name VARCHAR(120) DEFAULT NULL, + last_name VARCHAR(120) DEFAULT NULL, + job_title VARCHAR(120) DEFAULT NULL, + phone_primary VARCHAR(20) DEFAULT NULL, + phone_secondary VARCHAR(20) DEFAULT NULL, + email VARCHAR(180) DEFAULT NULL, + position INT DEFAULT 0 NOT 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_supplier_contact_name + CHECK (first_name IS NOT NULL OR last_name IS NOT NULL), + CONSTRAINT fk_supplier_contact_supplier + FOREIGN KEY (supplier_id) REFERENCES supplier (id) ON DELETE CASCADE, + CONSTRAINT fk_supplier_contact_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_supplier_contact_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + $this->addSql('CREATE INDEX idx_supplier_contact_supplier ON supplier_contact (supplier_id)'); + + $this->comment('supplier_contact', '_table', 'Contacts d un fournisseur (1:n) — au moins firstName OU lastName par contact (RG-2.04).'); + $this->comment('supplier_contact', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('supplier_contact', 'supplier_id', 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire du contact.'); + $this->comment('supplier_contact', 'first_name', 'Prenom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-2.04, chk_supplier_contact_name).'); + $this->comment('supplier_contact', 'last_name', 'Nom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-2.04, chk_supplier_contact_name).'); + $this->comment('supplier_contact', 'job_title', 'Fonction / intitule de poste du contact (≤ 120 caracteres).'); + $this->comment('supplier_contact', 'phone_primary', 'Telephone principal du contact — chiffres uniquement (normalisation serveur).'); + $this->comment('supplier_contact', 'phone_secondary', 'Telephone secondaire du contact — chiffres uniquement (normalisation serveur).'); + $this->comment('supplier_contact', 'email', 'Email du contact (lowercase serveur).'); + $this->comment('supplier_contact', 'position', 'Ordre d affichage du contact dans la liste du fournisseur (croissant).'); + $this->addTimestampableBlamableComments('supplier_contact'); + } + + // ================================================================= + // Sous-collection : adresses (1:n) + // ================================================================= + + private function createSupplierAddress(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE supplier_address ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + supplier_id INT NOT NULL, + address_type VARCHAR(20) NOT NULL, + country VARCHAR(80) DEFAULT 'France' NOT NULL, + postal_code VARCHAR(20) NOT NULL, + city VARCHAR(120) NOT NULL, + street VARCHAR(255) NOT NULL, + street_complement VARCHAR(255) DEFAULT NULL, + bennes INT DEFAULT NULL, + triage_provider BOOLEAN DEFAULT FALSE NOT NULL, + position INT DEFAULT 0 NOT 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_supplier_address_type + CHECK (address_type IN ('PROSPECT', 'DEPART', 'RENDU')), + CONSTRAINT fk_supplier_address_supplier + FOREIGN KEY (supplier_id) REFERENCES supplier (id) ON DELETE CASCADE, + CONSTRAINT fk_supplier_address_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_supplier_address_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + $this->addSql('CREATE INDEX idx_supplier_address_supplier ON supplier_address (supplier_id)'); + + $this->comment('supplier_address', '_table', 'Adresses d un fournisseur (1:n) — type PROSPECT/DEPART/RENDU exclusif (RG-2.09), >= 1 site rattache (RG-2.06).'); + $this->comment('supplier_address', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('supplier_address', 'supplier_id', 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire de l adresse.'); + $this->comment('supplier_address', 'address_type', 'Type d adresse : PROSPECT | DEPART | RENDU (radio exclusif par construction — RG-2.09, chk_supplier_address_type).'); + $this->comment('supplier_address', 'country', 'Pays de l adresse — defaut France.'); + $this->comment('supplier_address', 'postal_code', 'Code postal (4-5 chiffres attendus).'); + $this->comment('supplier_address', 'city', 'Ville — preremplie depuis le code postal via API BAN cote front.'); + $this->comment('supplier_address', 'street', 'Numero et voie de l adresse.'); + $this->comment('supplier_address', 'street_complement', 'Complement d adresse (etage, batiment...) — optionnel.'); + $this->comment('supplier_address', 'bennes', 'Nombre de bennes sur le site fournisseur (entier nullable) — specifique fournisseur.'); + $this->comment('supplier_address', 'triage_provider', 'Le fournisseur est prestataire de triage sur cette adresse. Faux par defaut.'); + $this->comment('supplier_address', 'position', 'Ordre d affichage de l adresse dans la liste du fournisseur (croissant).'); + $this->addTimestampableBlamableComments('supplier_address'); + } + + // ================================================================= + // Jointures de supplier_address (M2M) + // ================================================================= + + private function createSupplierAddressJoinTables(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE supplier_address_site ( + supplier_address_id INT NOT NULL, + site_id INT NOT NULL, + PRIMARY KEY (supplier_address_id, site_id), + CONSTRAINT fk_supplier_address_site_address + FOREIGN KEY (supplier_address_id) REFERENCES supplier_address (id) ON DELETE CASCADE, + CONSTRAINT fk_supplier_address_site_site + FOREIGN KEY (site_id) REFERENCES site (id) ON DELETE RESTRICT + ) + SQL); + $this->comment('supplier_address_site', '_table', 'Jointure M2M supplier_address <-> site (Sites) — sites rattaches a l adresse (>= 1 obligatoire, RG-2.06).'); + $this->comment('supplier_address_site', 'supplier_address_id', 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.'); + $this->comment('supplier_address_site', 'site_id', 'FK -> site.id, ON DELETE RESTRICT — site rattache a l adresse.'); + + $this->addSql(<<<'SQL' + CREATE TABLE supplier_address_contact ( + supplier_address_id INT NOT NULL, + supplier_contact_id INT NOT NULL, + PRIMARY KEY (supplier_address_id, supplier_contact_id), + CONSTRAINT fk_supplier_address_contact_address + FOREIGN KEY (supplier_address_id) REFERENCES supplier_address (id) ON DELETE CASCADE, + CONSTRAINT fk_supplier_address_contact_contact + FOREIGN KEY (supplier_contact_id) REFERENCES supplier_contact (id) ON DELETE CASCADE + ) + SQL); + $this->comment('supplier_address_contact', '_table', 'Jointure M2M supplier_address <-> supplier_contact — contacts associes a une adresse.'); + $this->comment('supplier_address_contact', 'supplier_address_id', 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.'); + $this->comment('supplier_address_contact', 'supplier_contact_id', 'FK -> supplier_contact.id, ON DELETE CASCADE — contact associe a l adresse.'); + + $this->addSql(<<<'SQL' + CREATE TABLE supplier_address_category ( + supplier_address_id INT NOT NULL, + category_id INT NOT NULL, + PRIMARY KEY (supplier_address_id, category_id), + CONSTRAINT fk_supplier_address_category_address + FOREIGN KEY (supplier_address_id) REFERENCES supplier_address (id) ON DELETE CASCADE, + CONSTRAINT fk_supplier_address_category_category + FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT + ) + SQL); + $this->comment('supplier_address_category', '_table', 'Jointure M2M supplier_address <-> category — categories d adresse de type FOURNISSEUR (RG-2.10).'); + $this->comment('supplier_address_category', 'supplier_address_id', 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.'); + $this->comment('supplier_address_category', 'category_id', 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type FOURNISSEUR (RG-2.10).'); + } + + // ================================================================= + // Sous-collection : RIB (1:n) + // ================================================================= + + private function createSupplierRib(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE supplier_rib ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + supplier_id INT NOT NULL, + label VARCHAR(120) NOT NULL, + bic VARCHAR(20) NOT NULL, + iban VARCHAR(34) NOT NULL, + position INT DEFAULT 0 NOT 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 fk_supplier_rib_supplier + FOREIGN KEY (supplier_id) REFERENCES supplier (id) ON DELETE CASCADE, + CONSTRAINT fk_supplier_rib_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_supplier_rib_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + $this->addSql('CREATE INDEX idx_supplier_rib_supplier ON supplier_rib (supplier_id)'); + + $this->comment('supplier_rib', '_table', 'Coordonnees bancaires d un fournisseur (1:n) — >= 1 RIB attendu selon le type de reglement (RG-2.08). Tous les champs audites (pas d AuditIgnore).'); + $this->comment('supplier_rib', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('supplier_rib', 'supplier_id', 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire du RIB.'); + $this->comment('supplier_rib', 'label', 'Libelle du RIB (ex: compte principal).'); + $this->comment('supplier_rib', 'bic', 'Code BIC/SWIFT de la banque (8 ou 11 caracteres).'); + $this->comment('supplier_rib', 'iban', 'IBAN du compte (≤ 34 caracteres).'); + $this->comment('supplier_rib', 'position', 'Ordre d affichage du RIB dans la liste du fournisseur (croissant).'); + $this->addTimestampableBlamableComments('supplier_rib'); + } + + // ================================================================= + // Helpers + // ================================================================= + + /** + * Pose les 4 commentaires standardises Timestampable/Blamable sur une table, + * en reutilisant le catalogue partage (source unique, cf. 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, + )); + } +}