|
|
|
@@ -0,0 +1,538 @@
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
|
|
|
|
|
|
use App\Shared\Infrastructure\Database\ColumnCommentsCatalog;
|
|
|
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
|
|
|
use Doctrine\Migrations\AbstractMigration;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* M1 — Repertoire clients (ERP-53) : creation de toute la structure BDD du
|
|
|
|
|
* module Commercial (clients + sous-collections + referentiels comptables).
|
|
|
|
|
*
|
|
|
|
|
* Tables creees :
|
|
|
|
|
* - Referentiels comptables (statiques, seedes ici) : tva_mode, payment_delay,
|
|
|
|
|
* payment_type, bank.
|
|
|
|
|
* - Table principale : client (formulaire + Information + Comptabilite +
|
|
|
|
|
* archive + soft-delete + Timestampable/Blamable).
|
|
|
|
|
* - Sous-collections : client_category (M2M), client_contact (1:n),
|
|
|
|
|
* client_address (1:n), client_rib (1:n).
|
|
|
|
|
* - Jointures de client_address : client_address_site, client_address_contact,
|
|
|
|
|
* client_address_category.
|
|
|
|
|
*
|
|
|
|
|
* Seed `category_type` (extension M0) : DISTRIBUTEUR / COURTIER / SECTEUR /
|
|
|
|
|
* AUTRE, en `ON CONFLICT (code) DO NOTHING` (idempotent — la table peut deja
|
|
|
|
|
* porter des donnees en prod). En dev/test, les fixtures purgent et re-seedent
|
|
|
|
|
* ces 4 types (cf. CategoryTypeFixtures) ; ce seed migration couvre la prod ou
|
|
|
|
|
* les fixtures ne tournent pas.
|
|
|
|
|
*
|
|
|
|
|
* Namespace racine `DoctrineMigrations` (regle ABSOLUE Starseed n°11) et NON
|
|
|
|
|
* `App\Module\Commercial\...` : avec plusieurs migrations_paths, Doctrine
|
|
|
|
|
* Migrations 3.x trie par FQCN alphabetique (AlphabeticalComparator → strcmp).
|
|
|
|
|
* Un namespace `App\Module\Commercial\...` trierait AVANT `DoctrineMigrations\...`
|
|
|
|
|
* et la migration s'executerait avant la creation de user/category/site sur
|
|
|
|
|
* base vide → echec des FK. Le namespace racine garantit l'ordre par timestamp.
|
|
|
|
|
*
|
|
|
|
|
* Style DDL aligne sur la migration M0 (Version20260527164000) plutot que sur
|
|
|
|
|
* le pseudo-SQL de la spec § 3.2 : `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-54).
|
|
|
|
|
*
|
|
|
|
|
* Decision Q4 (29/05/2026) : unicite metier sur le NOM DE SOCIETE uniquement.
|
|
|
|
|
* Pas d'index unique sur siren ni email (RG-1.15 / RG-1.17 supprimees).
|
|
|
|
|
*
|
|
|
|
|
* Chaque colonne porte un `COMMENT ON COLUMN` (regle ABSOLUE n°12, garde-fou
|
|
|
|
|
* ColumnsHaveSqlCommentTest). Les tables n'etant pas encore mappees par l'ORM,
|
|
|
|
|
* ces commentaires survivent au `schema:update --force` du setup de test.
|
|
|
|
|
*/
|
|
|
|
|
final class Version20260601000000 extends AbstractMigration
|
|
|
|
|
{
|
|
|
|
|
public function getDescription(): string
|
|
|
|
|
{
|
|
|
|
|
return 'ERP-53 (M1) : tables client + sous-collections + referentiels comptables + seed category_type.';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function up(Schema $schema): void
|
|
|
|
|
{
|
|
|
|
|
$this->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,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|