From 9f96d1c40d3de33e47bf9974fa36ee8b1db5fdfd Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 29 May 2026 14:48:40 +0200 Subject: [PATCH] feat(commercial) : migrate M1 client tables + accounting referentials + extend category_type seed --- migrations/Version20260601000000.php | 538 ++++++++++++++++++ .../DataFixtures/CategoryTypeFixtures.php | 63 ++ 2 files changed, 601 insertions(+) create mode 100644 migrations/Version20260601000000.php create mode 100644 src/Module/Catalog/Infrastructure/DataFixtures/CategoryTypeFixtures.php diff --git a/migrations/Version20260601000000.php b/migrations/Version20260601000000.php new file mode 100644 index 0000000..af26227 --- /dev/null +++ b/migrations/Version20260601000000.php @@ -0,0 +1,538 @@ +createAccountingReferentials(); + $this->createClientTable(); + $this->createClientCategory(); + $this->createClientContact(); + $this->createClientAddress(); + $this->createClientAddressJoinTables(); + $this->createClientRib(); + $this->seedCategoryTypes(); + } + + public function down(Schema $schema): void + { + // Ordre inverse des dependances FK : on supprime d'abord les jointures + // et sous-collections, puis client, puis les referentiels. + $this->addSql('DROP TABLE client_address_category'); + $this->addSql('DROP TABLE client_address_contact'); + $this->addSql('DROP TABLE client_address_site'); + $this->addSql('DROP TABLE client_rib'); + $this->addSql('DROP TABLE client_address'); + $this->addSql('DROP TABLE client_contact'); + $this->addSql('DROP TABLE client_category'); + $this->addSql('DROP TABLE client'); + $this->addSql('DROP TABLE bank'); + $this->addSql('DROP TABLE payment_type'); + $this->addSql('DROP TABLE payment_delay'); + $this->addSql('DROP TABLE tva_mode'); + + // Retire uniquement les 4 types seedes par cette migration. Les autres + // types eventuels (CRUD futur) sont preserves. + $this->addSql(<<<'SQL' + DELETE FROM category_type WHERE code IN ('DISTRIBUTEUR', 'COURTIER', 'SECTEUR', 'AUTRE') + SQL); + } + + // ================================================================= + // Referentiels comptables (4 tables statiques, memes colonnes) + // ================================================================= + + private function createAccountingReferentials(): void + { + $referentials = [ + 'tva_mode' => 'Referentiel des modes de TVA appliques a un client (France, Export, Intracom).', + 'payment_delay' => 'Referentiel des delais de reglement (15 jours, 30 jours, a reception).', + 'payment_type' => 'Referentiel des types de reglement (virement, LCR, cheque, non soumise).', + 'bank' => 'Referentiel des banques selectionnables pour le reglement par virement.', + ]; + + foreach ($referentials as $table => $tableComment) { + $this->addSql(sprintf(<<<'SQL' + CREATE TABLE %s ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + code VARCHAR(30) NOT NULL, + label VARCHAR(120) NOT NULL, + position INT DEFAULT 0 NOT NULL, + PRIMARY KEY (id) + ) + SQL, $table)); + $this->addSql(sprintf('CREATE UNIQUE INDEX uq_%s_code ON %s (code)', $table, $table)); + + $this->comment($table, '_table', $tableComment); + $this->comment($table, 'id', 'Identifiant interne auto-incremente.'); + $this->comment($table, 'code', 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.'); + $this->comment($table, 'label', 'Libelle affichable (FR, ≤ 120 caracteres).'); + $this->comment($table, 'position', 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).'); + } + + // Seed initial (cf. spec § 3.2). Tables fraichement creees donc vides : + // INSERT direct sans ON CONFLICT. + $this->addSql(<<<'SQL' + INSERT INTO tva_mode (code, label, position) VALUES + ('FRANCE_VENTES', 'France (ventes)', 10), + ('EXPORT_VENTES', 'Export (ventes)', 20), + ('INTRACOM_VENTES', 'Intracom (ventes)', 30) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO payment_delay (code, label, position) VALUES + ('J15', '15 jours', 10), + ('J30', '30 jours', 20), + ('A_RECEPTION', 'À réception', 30) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO payment_type (code, label, position) VALUES + ('VIREMENT', 'Virement', 10), + ('LCR', 'LCR', 20), + ('NON_SOUMISE', 'Non soumise', 30), + ('CHEQUE', 'Chèque', 40) + SQL); + $this->addSql(<<<'SQL' + INSERT INTO bank (code, label, position) VALUES + ('SG', 'Société Générale', 10), + ('CIC', 'CIC', 20), + ('CA', 'Crédit Agricole', 30) + SQL); + } + + // ================================================================= + // Table principale `client` + // ================================================================= + + private function createClientTable(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE client ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + company_name VARCHAR(180) NOT NULL, + first_name VARCHAR(120) DEFAULT NULL, + last_name VARCHAR(120) DEFAULT NULL, + phone_primary VARCHAR(20) NOT NULL, + phone_secondary VARCHAR(20) DEFAULT NULL, + email VARCHAR(180) NOT NULL, + distributor_id INT DEFAULT NULL, + broker_id INT DEFAULT NULL, + triage_service BOOLEAN DEFAULT FALSE 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, + 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 chk_client_distrib_or_broker + CHECK (NOT (distributor_id IS NOT NULL AND broker_id IS NOT NULL)), + CONSTRAINT fk_client_distributor + FOREIGN KEY (distributor_id) REFERENCES client (id) ON DELETE SET NULL, + CONSTRAINT fk_client_broker + FOREIGN KEY (broker_id) REFERENCES client (id) ON DELETE SET NULL, + CONSTRAINT fk_client_tva_mode + FOREIGN KEY (tva_mode_id) REFERENCES tva_mode (id) ON DELETE RESTRICT, + CONSTRAINT fk_client_payment_delay + FOREIGN KEY (payment_delay_id) REFERENCES payment_delay (id) ON DELETE RESTRICT, + CONSTRAINT fk_client_payment_type + FOREIGN KEY (payment_type_id) REFERENCES payment_type (id) ON DELETE RESTRICT, + CONSTRAINT fk_client_bank + FOREIGN KEY (bank_id) REFERENCES bank (id) ON DELETE RESTRICT, + CONSTRAINT fk_client_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_client_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + + $this->addSql('CREATE INDEX idx_client_is_archived ON client (is_archived)'); + $this->addSql('CREATE INDEX idx_client_deleted_at ON client (deleted_at)'); + $this->addSql('CREATE INDEX idx_client_distributor_id ON client (distributor_id)'); + $this->addSql('CREATE INDEX idx_client_broker_id ON client (broker_id)'); + $this->addSql('CREATE INDEX idx_client_created_by ON client (created_by)'); + $this->addSql('CREATE INDEX idx_client_updated_by ON client (updated_by)'); + + // Unicite metier partielle (Q4) : nom de societe insensible a la casse, + // parmi les non-archives ET non soft-deletes uniquement. Pas d'index + // unique sur siren ni email. + $this->addSql(<<<'SQL' + CREATE UNIQUE INDEX uq_client_company_name_active + ON client (LOWER(company_name)) + WHERE is_archived = FALSE AND deleted_at IS NULL + SQL); + + $this->comment('client', '_table', 'Repertoire clients (M1 Commercial) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M2).'); + $this->comment('client', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('client', 'company_name', 'Raison sociale (stockee en MAJUSCULES, RG-1.18). Unique case-insensitive parmi les actifs non archives/non supprimes (RG-1.16, uq_client_company_name_active).'); + $this->comment('client', 'first_name', 'Prenom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).'); + $this->comment('client', 'last_name', 'Nom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).'); + $this->comment('client', 'phone_primary', 'Telephone principal — stocke en chiffres uniquement (RG-1.20). Obligatoire.'); + $this->comment('client', 'phone_secondary', 'Telephone secondaire optionnel — chiffres uniquement (RG-1.20).'); + $this->comment('client', 'email', 'Email principal (lowercase serveur, RG-1.21). NON unique (RG-1.17 supprimee, Q4).'); + $this->comment('client', 'distributor_id', 'FK auto-referente vers un client porteur de la categorie DISTRIBUTEUR — exclusive avec broker_id (RG-1.03, chk_client_distrib_or_broker). FK -> client.id, ON DELETE SET NULL.'); + $this->comment('client', 'broker_id', 'FK auto-referente vers un client porteur de la categorie COURTIER — exclusive avec distributor_id (RG-1.03). FK -> client.id, ON DELETE SET NULL.'); + $this->comment('client', 'triage_service', 'Drapeau service triage active pour le client. Faux par defaut.'); + $this->comment('client', 'description', 'Onglet Information : description libre. Obligatoire pour le role Commerciale (RG-1.04), optionnel sinon.'); + $this->comment('client', 'competitors', 'Onglet Information : concurrents identifies (texte libre ≤ 255). Obligatoire role Commerciale (RG-1.04).'); + $this->comment('client', 'founded_at', 'Onglet Information : date de creation de l entreprise. Obligatoire role Commerciale (RG-1.04).'); + $this->comment('client', 'employees_count', 'Onglet Information : effectif (entier >= 0). Obligatoire role Commerciale (RG-1.04).'); + $this->comment('client', 'revenue_amount', 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Obligatoire role Commerciale (RG-1.04).'); + $this->comment('client', 'director_name', 'Onglet Information : nom du dirigeant. Obligatoire role Commerciale (RG-1.04).'); + $this->comment('client', 'profit_amount', 'Onglet Information : resultat / benefice (NUMERIC 15,2). Obligatoire role Commerciale (RG-1.04).'); + $this->comment('client', 'siren', 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (RG-1.15 supprimee, Q4).'); + $this->comment('client', 'account_number', 'Onglet Comptabilite : numero de compte comptable du client.'); + $this->comment('client', 'tva_mode_id', 'Onglet Comptabilite : mode de TVA applique — FK -> tva_mode.id, ON DELETE RESTRICT.'); + $this->comment('client', 'n_tva', 'Onglet Comptabilite : numero de TVA intracommunautaire.'); + $this->comment('client', 'payment_delay_id', 'Onglet Comptabilite : delai de reglement — FK -> payment_delay.id, ON DELETE RESTRICT.'); + $this->comment('client', 'payment_type_id', 'Onglet Comptabilite : type de reglement — FK -> payment_type.id, ON DELETE RESTRICT. Code LCR impose >= 1 RIB (RG-1.13), VIREMENT impose une banque (RG-1.12).'); + $this->comment('client', 'bank_id', 'Onglet Comptabilite : banque — FK -> bank.id, ON DELETE RESTRICT. Obligatoire si payment_type = VIREMENT (RG-1.12).'); + $this->comment('client', 'is_archived', 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission commercial.clients.archive (RG-1.22/23).'); + $this->comment('client', 'archived_at', 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration (RG-1.22/23).'); + $this->comment('client', 'deleted_at', 'Horodatage du soft-delete technique (HP M2) — non expose par l API au M1. Null = ligne active.'); + $this->addTimestampableBlamableComments('client'); + } + + // ================================================================= + // M2M client <-> category + // ================================================================= + + private function createClientCategory(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE client_category ( + client_id INT NOT NULL, + category_id INT NOT NULL, + PRIMARY KEY (client_id, category_id), + CONSTRAINT fk_client_category_client + FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE, + CONSTRAINT fk_client_category_category + FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT + ) + SQL); + $this->addSql('CREATE INDEX idx_client_category_category ON client_category (category_id)'); + + $this->comment('client_category', '_table', 'Jointure M2M client <-> category (Catalog) — categories metier du client (au moins une obligatoire).'); + $this->comment('client_category', 'client_id', 'FK -> client.id, ON DELETE CASCADE — client porteur de la categorie.'); + $this->comment('client_category', 'category_id', 'FK -> category.id, ON DELETE RESTRICT — categorie rattachee au client.'); + } + + // ================================================================= + // Sous-collection : contacts (1:n) + // ================================================================= + + private function createClientContact(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE client_contact ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + client_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_client_contact_name + CHECK (first_name IS NOT NULL OR last_name IS NOT NULL), + CONSTRAINT fk_client_contact_client + FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE, + CONSTRAINT fk_client_contact_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_client_contact_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + $this->addSql('CREATE INDEX idx_client_contact_client ON client_contact (client_id)'); + + $this->comment('client_contact', '_table', 'Contacts d un client (1:n) — au moins firstName OU lastName par contact (RG-1.05).'); + $this->comment('client_contact', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('client_contact', 'client_id', 'FK -> client.id, ON DELETE CASCADE — client proprietaire du contact.'); + $this->comment('client_contact', 'first_name', 'Prenom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-1.05, chk_client_contact_name).'); + $this->comment('client_contact', 'last_name', 'Nom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-1.05, chk_client_contact_name).'); + $this->comment('client_contact', 'job_title', 'Fonction / intitule de poste du contact (≤ 120 caracteres).'); + $this->comment('client_contact', 'phone_primary', 'Telephone principal du contact — chiffres uniquement (RG-1.20).'); + $this->comment('client_contact', 'phone_secondary', 'Telephone secondaire du contact — chiffres uniquement (RG-1.20).'); + $this->comment('client_contact', 'email', 'Email du contact (lowercase serveur, RG-1.21).'); + $this->comment('client_contact', 'position', 'Ordre d affichage du contact dans la liste du client (croissant).'); + $this->addTimestampableBlamableComments('client_contact'); + } + + // ================================================================= + // Sous-collection : adresses (1:n) + // ================================================================= + + private function createClientAddress(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE client_address ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + client_id INT NOT NULL, + is_prospect BOOLEAN DEFAULT FALSE NOT NULL, + is_delivery BOOLEAN DEFAULT FALSE NOT NULL, + is_billing BOOLEAN DEFAULT FALSE 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, + billing_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_client_address_prospect_exclusive + CHECK (NOT (is_prospect = TRUE AND (is_delivery = TRUE OR is_billing = TRUE))), + CONSTRAINT chk_client_address_billing_email + CHECK ((is_billing = FALSE AND billing_email IS NULL) + OR (is_billing = TRUE AND billing_email IS NOT NULL)), + CONSTRAINT fk_client_address_client + FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE, + CONSTRAINT fk_client_address_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_client_address_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + $this->addSql('CREATE INDEX idx_client_address_client ON client_address (client_id)'); + + $this->comment('client_address', '_table', 'Adresses d un client (1:n) — prospect exclusif de livraison/facturation (RG-1.06/07/08), >= 1 site rattache (RG-1.10).'); + $this->comment('client_address', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('client_address', 'client_id', 'FK -> client.id, ON DELETE CASCADE — client proprietaire de l adresse.'); + $this->comment('client_address', 'is_prospect', 'Adresse de prospection — exclusive de is_delivery/is_billing (RG-1.06/07/08, chk_client_address_prospect_exclusive). Faux par defaut.'); + $this->comment('client_address', 'is_delivery', 'Adresse de livraison. Exclusive de is_prospect. Faux par defaut.'); + $this->comment('client_address', 'is_billing', 'Adresse de facturation. Exclusive de is_prospect. Impose billing_email (RG-1.11). Faux par defaut.'); + $this->comment('client_address', 'country', 'Pays de l adresse — defaut France.'); + $this->comment('client_address', 'postal_code', 'Code postal (4-5 chiffres attendus, RG-1.09).'); + $this->comment('client_address', 'city', 'Ville — preremplie depuis le code postal via API BAN cote front (RG-1.09).'); + $this->comment('client_address', 'street', 'Numero et voie de l adresse.'); + $this->comment('client_address', 'street_complement', 'Complement d adresse (etage, batiment...) — optionnel.'); + $this->comment('client_address', 'billing_email', 'Email de facturation — obligatoire si is_billing, null sinon (RG-1.11, chk_client_address_billing_email).'); + $this->comment('client_address', 'position', 'Ordre d affichage de l adresse dans la liste du client (croissant).'); + $this->addTimestampableBlamableComments('client_address'); + } + + // ================================================================= + // Jointures de client_address (M2M) + // ================================================================= + + private function createClientAddressJoinTables(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE client_address_site ( + client_address_id INT NOT NULL, + site_id INT NOT NULL, + PRIMARY KEY (client_address_id, site_id), + CONSTRAINT fk_client_address_site_address + FOREIGN KEY (client_address_id) REFERENCES client_address (id) ON DELETE CASCADE, + CONSTRAINT fk_client_address_site_site + FOREIGN KEY (site_id) REFERENCES site (id) ON DELETE RESTRICT + ) + SQL); + $this->comment('client_address_site', '_table', 'Jointure M2M client_address <-> site (Sites) — sites desservis par l adresse (>= 1 obligatoire, RG-1.10).'); + $this->comment('client_address_site', 'client_address_id', 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.'); + $this->comment('client_address_site', 'site_id', 'FK -> site.id, ON DELETE RESTRICT — site rattache a l adresse.'); + + $this->addSql(<<<'SQL' + CREATE TABLE client_address_contact ( + client_address_id INT NOT NULL, + client_contact_id INT NOT NULL, + PRIMARY KEY (client_address_id, client_contact_id), + CONSTRAINT fk_client_address_contact_address + FOREIGN KEY (client_address_id) REFERENCES client_address (id) ON DELETE CASCADE, + CONSTRAINT fk_client_address_contact_contact + FOREIGN KEY (client_contact_id) REFERENCES client_contact (id) ON DELETE CASCADE + ) + SQL); + $this->comment('client_address_contact', '_table', 'Jointure M2M client_address <-> client_contact — contacts associes a une adresse.'); + $this->comment('client_address_contact', 'client_address_id', 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.'); + $this->comment('client_address_contact', 'client_contact_id', 'FK -> client_contact.id, ON DELETE CASCADE — contact associe a l adresse.'); + + $this->addSql(<<<'SQL' + CREATE TABLE client_address_category ( + client_address_id INT NOT NULL, + category_id INT NOT NULL, + PRIMARY KEY (client_address_id, category_id), + CONSTRAINT fk_client_address_category_address + FOREIGN KEY (client_address_id) REFERENCES client_address (id) ON DELETE CASCADE, + CONSTRAINT fk_client_address_category_category + FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE RESTRICT + ) + SQL); + $this->comment('client_address_category', '_table', 'Jointure M2M client_address <-> category — categories d adresse (types SECTEUR/AUTRE uniquement, RG-1.29).'); + $this->comment('client_address_category', 'client_address_id', 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.'); + $this->comment('client_address_category', 'category_id', 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse (type SECTEUR ou AUTRE, RG-1.29).'); + } + + // ================================================================= + // Sous-collection : RIB (1:n) + // ================================================================= + + private function createClientRib(): void + { + $this->addSql(<<<'SQL' + CREATE TABLE client_rib ( + id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, + client_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_client_rib_client + FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE CASCADE, + CONSTRAINT fk_client_rib_created_by + FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL, + CONSTRAINT fk_client_rib_updated_by + FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL + ) + SQL); + $this->addSql('CREATE INDEX idx_client_rib_client ON client_rib (client_id)'); + + $this->comment('client_rib', '_table', 'Coordonnees bancaires d un client (1:n) — >= 1 RIB obligatoire si payment_type = LCR (RG-1.13). Tous les champs audites (pas d AuditIgnore).'); + $this->comment('client_rib', 'id', 'Identifiant interne auto-incremente.'); + $this->comment('client_rib', 'client_id', 'FK -> client.id, ON DELETE CASCADE — client proprietaire du RIB.'); + $this->comment('client_rib', 'label', 'Libelle du RIB (ex: compte principal).'); + $this->comment('client_rib', 'bic', 'Code BIC/SWIFT de la banque (8 ou 11 caracteres).'); + $this->comment('client_rib', 'iban', 'IBAN du compte (≤ 34 caracteres).'); + $this->comment('client_rib', 'position', 'Ordre d affichage du RIB dans la liste du client (croissant).'); + $this->addTimestampableBlamableComments('client_rib'); + } + + // ================================================================= + // Seed extension category_type (M0) + // ================================================================= + + private function seedCategoryTypes(): void + { + // Idempotent : la table category_type peut deja porter des donnees en + // prod. ON CONFLICT (code) s appuie sur l index unique uq_category_type_code. + // NB : la table M0 n a pas de colonne `position` (id/code/label seulement), + // contrairement au pseudo-SQL de la spec § 3.3. + $this->addSql(<<<'SQL' + INSERT INTO category_type (code, label) VALUES + ('DISTRIBUTEUR', 'Distributeur'), + ('COURTIER', 'Courtier'), + ('SECTEUR', 'Secteur'), + ('AUTRE', 'Autre') + ON CONFLICT (code) DO NOTHING + SQL); + } + + // ================================================================= + // 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, + )); + } +} diff --git a/src/Module/Catalog/Infrastructure/DataFixtures/CategoryTypeFixtures.php b/src/Module/Catalog/Infrastructure/DataFixtures/CategoryTypeFixtures.php new file mode 100644 index 0000000..43b7686 --- /dev/null +++ b/src/Module/Catalog/Infrastructure/DataFixtures/CategoryTypeFixtures.php @@ -0,0 +1,63 @@ + libelle FR. + * Doit rester aligne sur le seed de la migration Version20260601000000. + */ + private const TYPES = [ + 'DISTRIBUTEUR' => 'Distributeur', + 'COURTIER' => 'Courtier', + 'SECTEUR' => 'Secteur', + 'AUTRE' => 'Autre', + ]; + + public function __construct( + private readonly CategoryTypeRepositoryInterface $categoryTypeRepository, + ) {} + + public function load(ObjectManager $manager): void + { + // Index des types deja presents par code, pour ne pas creer de doublon. + $existingByCode = []; + foreach ($this->categoryTypeRepository->findAllOrderedByLabel() as $type) { + $existingByCode[$type->getCode()] = $type; + } + + foreach (self::TYPES as $code => $label) { + $type = $existingByCode[$code] ?? new CategoryType(); + $type->setCode($code); + $type->setLabel($label); + $manager->persist($type); + } + + $manager->flush(); + } +}