feat(commercial) : add M1 client entities + accounting referentials + repositories
Entites metier (Client, ClientContact, ClientAddress, ClientRib) avec #[Auditable] + Timestampable/Blamable, et 4 referentiels comptables statiques (TvaMode, PaymentDelay, PaymentType, Bank). 8 repositories interfaces + impl Doctrine. Aucun ApiResource (Provider/Processor = ERP-55). - Client : 2 FK auto-referentes distributor/broker (mutuellement exclusives, CHECK en base), M2M categories, FK referentiels comptables, groupes de serialisation par onglet. Pas de #[ORM\UniqueConstraint] : unicite du nom de societe portee par l'index partiel Postgres (decision Q4). - ClientRib : tous les champs audites, aucun #[AuditIgnore] sur iban/bic (decision 29/05, audit admin-only). - M2M Category via le contrat Shared CategoryInterface + resolve_target_entities (regle n°1, pas d'import inter-modules) ; sites via SiteInterface. - CommercialReferentialFixtures : re-seed idempotent des 4 referentiels (sinon vides apres db-reset car desormais tables mappees, purgees par les fixtures). - Referentiels whitelistes dans EntitiesAreTimestampableBlamableTest::EXCLUDED. - doctrine.yaml : mapping ORM du module Commercial + resolve CategoryInterface. - ColumnCommentsCatalog : ajout des colonnes M1 (chemin schema:update/test) ; migration retrofit Version20260528120000 filtree sur les tables existantes pour ne pas casser sur les tables des modules crees plus tard. - makefile test-db-setup : recreation de l'index partiel uq_client_company_name_active. Refs ERP-54.
This commit is contained in:
@@ -128,6 +128,135 @@ final class ColumnCommentsCatalog
|
||||
'user_id' => 'FK -> user.id, ON DELETE CASCADE — utilisateur ayant acces au site.',
|
||||
'site_id' => 'FK -> site.id, ON DELETE CASCADE — site accessible par l utilisateur.',
|
||||
],
|
||||
|
||||
// === M1 Commercial (ERP-53/54) — miroir des COMMENT de la migration
|
||||
// Version20260601000000 pour le chemin schema:update (dev/test). ===
|
||||
|
||||
'tva_mode' => [
|
||||
'_table' => 'Referentiel des modes de TVA appliques a un client (France, Export, Intracom).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'code' => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
|
||||
'label' => 'Libelle affichable (FR, ≤ 120 caracteres).',
|
||||
'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
|
||||
],
|
||||
|
||||
'payment_delay' => [
|
||||
'_table' => 'Referentiel des delais de reglement (15 jours, 30 jours, a reception).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'code' => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
|
||||
'label' => 'Libelle affichable (FR, ≤ 120 caracteres).',
|
||||
'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
|
||||
],
|
||||
|
||||
'payment_type' => [
|
||||
'_table' => 'Referentiel des types de reglement (virement, LCR, cheque, non soumise).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'code' => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
|
||||
'label' => 'Libelle affichable (FR, ≤ 120 caracteres).',
|
||||
'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
|
||||
],
|
||||
|
||||
'bank' => [
|
||||
'_table' => 'Referentiel des banques selectionnables pour le reglement par virement.',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'code' => 'Code technique stable (UPPER_SNAKE, ≤ 30 caracteres) — unique, utilise par le code metier.',
|
||||
'label' => 'Libelle affichable (FR, ≤ 120 caracteres).',
|
||||
'position' => 'Ordre d affichage croissant dans les selecteurs (tri position ASC puis label ASC).',
|
||||
],
|
||||
|
||||
'client' => [
|
||||
'_table' => 'Repertoire clients (M1 Commercial) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M2).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'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).',
|
||||
'first_name' => 'Prenom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).',
|
||||
'last_name' => 'Nom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).',
|
||||
'phone_primary' => 'Telephone principal — stocke en chiffres uniquement (RG-1.20). Obligatoire.',
|
||||
'phone_secondary' => 'Telephone secondaire optionnel — chiffres uniquement (RG-1.20).',
|
||||
'email' => 'Email principal (lowercase serveur, RG-1.21). NON unique (RG-1.17 supprimee, Q4).',
|
||||
'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.',
|
||||
'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.',
|
||||
'triage_service' => 'Drapeau service triage active pour le client. Faux par defaut.',
|
||||
'description' => 'Onglet Information : description libre. Obligatoire pour le role Commerciale (RG-1.04), optionnel sinon.',
|
||||
'competitors' => 'Onglet Information : concurrents identifies (texte libre ≤ 255). Obligatoire role Commerciale (RG-1.04).',
|
||||
'founded_at' => 'Onglet Information : date de creation de l entreprise. Obligatoire role Commerciale (RG-1.04).',
|
||||
'employees_count' => 'Onglet Information : effectif (entier >= 0). Obligatoire role Commerciale (RG-1.04).',
|
||||
'revenue_amount' => 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Obligatoire role Commerciale (RG-1.04).',
|
||||
'director_name' => 'Onglet Information : nom du dirigeant. Obligatoire role Commerciale (RG-1.04).',
|
||||
'profit_amount' => 'Onglet Information : resultat / benefice (NUMERIC 15,2). Obligatoire role Commerciale (RG-1.04).',
|
||||
'siren' => 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (RG-1.15 supprimee, Q4).',
|
||||
'account_number' => 'Onglet Comptabilite : numero de compte comptable du client.',
|
||||
'tva_mode_id' => 'Onglet Comptabilite : mode de TVA applique — FK -> tva_mode.id, ON DELETE RESTRICT.',
|
||||
'n_tva' => 'Onglet Comptabilite : numero de TVA intracommunautaire.',
|
||||
'payment_delay_id' => 'Onglet Comptabilite : delai de reglement — FK -> payment_delay.id, ON DELETE RESTRICT.',
|
||||
'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).',
|
||||
'bank_id' => 'Onglet Comptabilite : banque — FK -> bank.id, ON DELETE RESTRICT. Obligatoire si payment_type = VIREMENT (RG-1.12).',
|
||||
'is_archived' => 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission commercial.clients.archive (RG-1.22/23).',
|
||||
'archived_at' => 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration (RG-1.22/23).',
|
||||
'deleted_at' => 'Horodatage du soft-delete technique (HP M2) — non expose par l API au M1. Null = ligne active.',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
'client_category' => [
|
||||
'_table' => 'Jointure M2M client <-> category (Catalog) — categories metier du client (au moins une obligatoire).',
|
||||
'client_id' => 'FK -> client.id, ON DELETE CASCADE — client porteur de la categorie.',
|
||||
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie rattachee au client.',
|
||||
],
|
||||
|
||||
'client_contact' => [
|
||||
'_table' => 'Contacts d un client (1:n) — au moins firstName OU lastName par contact (RG-1.05).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'client_id' => 'FK -> client.id, ON DELETE CASCADE — client proprietaire du contact.',
|
||||
'first_name' => 'Prenom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-1.05, chk_client_contact_name).',
|
||||
'last_name' => 'Nom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-1.05, chk_client_contact_name).',
|
||||
'job_title' => 'Fonction / intitule de poste du contact (≤ 120 caracteres).',
|
||||
'phone_primary' => 'Telephone principal du contact — chiffres uniquement (RG-1.20).',
|
||||
'phone_secondary' => 'Telephone secondaire du contact — chiffres uniquement (RG-1.20).',
|
||||
'email' => 'Email du contact (lowercase serveur, RG-1.21).',
|
||||
'position' => 'Ordre d affichage du contact dans la liste du client (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
'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).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'client_id' => 'FK -> client.id, ON DELETE CASCADE — client proprietaire de l adresse.',
|
||||
'is_prospect' => 'Adresse de prospection — exclusive de is_delivery/is_billing (RG-1.06/07/08, chk_client_address_prospect_exclusive). Faux par defaut.',
|
||||
'is_delivery' => 'Adresse de livraison. Exclusive de is_prospect. Faux par defaut.',
|
||||
'is_billing' => 'Adresse de facturation. Exclusive de is_prospect. Impose billing_email (RG-1.11). Faux par defaut.',
|
||||
'country' => 'Pays de l adresse — defaut France.',
|
||||
'postal_code' => 'Code postal (4-5 chiffres attendus, RG-1.09).',
|
||||
'city' => 'Ville — preremplie depuis le code postal via API BAN cote front (RG-1.09).',
|
||||
'street' => 'Numero et voie de l adresse.',
|
||||
'street_complement' => 'Complement d adresse (etage, batiment...) — optionnel.',
|
||||
'billing_email' => 'Email de facturation — obligatoire si is_billing, null sinon (RG-1.11, chk_client_address_billing_email).',
|
||||
'position' => 'Ordre d affichage de l adresse dans la liste du client (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
'client_address_site' => [
|
||||
'_table' => 'Jointure M2M client_address <-> site (Sites) — sites desservis par l adresse (>= 1 obligatoire, RG-1.10).',
|
||||
'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
|
||||
'site_id' => 'FK -> site.id, ON DELETE RESTRICT — site rattache a l adresse.',
|
||||
],
|
||||
|
||||
'client_address_contact' => [
|
||||
'_table' => 'Jointure M2M client_address <-> client_contact — contacts associes a une adresse.',
|
||||
'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
|
||||
'client_contact_id' => 'FK -> client_contact.id, ON DELETE CASCADE — contact associe a l adresse.',
|
||||
],
|
||||
|
||||
'client_address_category' => [
|
||||
'_table' => 'Jointure M2M client_address <-> category — categories d adresse (types SECTEUR/AUTRE uniquement, RG-1.29).',
|
||||
'client_address_id' => 'FK -> client_address.id, ON DELETE CASCADE — adresse concernee.',
|
||||
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse (type SECTEUR ou AUTRE, RG-1.29).',
|
||||
],
|
||||
|
||||
'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).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'client_id' => 'FK -> client.id, ON DELETE CASCADE — client proprietaire du RIB.',
|
||||
'label' => 'Libelle du RIB (ex: compte principal).',
|
||||
'bic' => 'Code BIC/SWIFT de la banque (8 ou 11 caracteres).',
|
||||
'iban' => 'IBAN du compte (≤ 34 caracteres).',
|
||||
'position' => 'Ordre d affichage du RIB dans la liste du client (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -151,12 +280,25 @@ final class ColumnCommentsCatalog
|
||||
* Construit la liste des requetes SQL `COMMENT ON TABLE/COLUMN` (en
|
||||
* dollar-quoting Postgres `$_$`) a partir du catalogue.
|
||||
*
|
||||
* @param null|list<string> $onlyTables Restreint la generation a ces tables
|
||||
* (utile pour la migration retrofit qui
|
||||
* ne doit commenter que les tables deja
|
||||
* presentes a son instant T — les tables
|
||||
* des modules crees plus tard posent
|
||||
* leurs propres COMMENT). null = tout.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function toSqlStatements(): array
|
||||
public static function toSqlStatements(?array $onlyTables = null): array
|
||||
{
|
||||
$allowed = null === $onlyTables ? null : array_fill_keys($onlyTables, true);
|
||||
|
||||
$statements = [];
|
||||
foreach (self::comments() as $table => $entries) {
|
||||
if (null !== $allowed && !isset($allowed[$table])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quotedTable = self::quoteIdent($table);
|
||||
foreach ($entries as $column => $description) {
|
||||
if ('_table' === $column) {
|
||||
|
||||
Reference in New Issue
Block a user