From 9618974b70f8e1871db9d7f2000ebdc94e07adb1 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 29 May 2026 11:54:57 +0200 Subject: [PATCH] =?UTF-8?q?docs(commercial)=20:=20Q4=20unicit=C3=A9=20limi?= =?UTF-8?q?t=C3=A9e=20au=20nom=20de=20soci=C3=A9t=C3=A9=20+=20fix=20onglet?= =?UTF-8?q?=20compta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/specs/M1-clients/spec-back.md | 34 ++++++++++++----------------- docs/specs/M1-clients/spec-front.md | 6 ++--- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/specs/M1-clients/spec-back.md b/docs/specs/M1-clients/spec-back.md index 82477c1..3c6d5b7 100644 --- a/docs/specs/M1-clients/spec-back.md +++ b/docs/specs/M1-clients/spec-back.md @@ -108,13 +108,13 @@ Le M1 expose **uniquement le mécanisme Archive**. Le soft delete reste prépar - PATCH `{ "isArchived": true }` archive ; PATCH `{ "isArchived": false }` restaure. - Les unicités métier (SIREN, nom, email) ignorent les archivés ET les soft-deletés (cf. § 3.5). -### 2.4 Unicité partielle Postgres — 3 contraintes sur Client +### 2.4 Unicité partielle Postgres — 1 contrainte sur Client -Index uniques partiels (`WHERE is_archived = false AND deleted_at IS NULL`) sur : +> **Décision Q4 (29/05/2026, Matthieu)** : l'unicité métier porte **uniquement sur le nom de société**. Le SIREN et l'email principal ne sont **pas** uniques (un même email peut servir plusieurs clients ; un SIREN peut être partagé entre établissements). -1. `LOWER(company_name)` — unicité du nom d'entreprise -2. `siren` — unicité du SIREN -3. `LOWER(email)` — unicité de l'email principal +Index unique partiel (`WHERE is_archived = false AND deleted_at IS NULL`) sur : + +1. `LOWER(company_name)` — unicité du nom d'entreprise (case-insensitive, parmi non-archivés et non soft-deletés) Tentative de doublon → `409 Conflict` géré par le `ClientProcessor` qui attrape la `UniqueConstraintViolationException`. @@ -346,19 +346,13 @@ CREATE INDEX idx_client_broker_id ON client(broker_id); CREATE INDEX idx_client_created_by ON client(created_by); CREATE INDEX idx_client_updated_by ON client(updated_by); --- Unicités métier (partielles : on ignore archives + soft-delete) +-- Unicité métier (partielle : on ignore archives + soft-delete) +-- Décision Q4 (29/05/2026) : unicité sur le nom de société UNIQUEMENT. +-- SIREN et email NE SONT PAS uniques (pas d'index uq_client_siren_active ni uq_client_email_active). CREATE UNIQUE INDEX uq_client_company_name_active ON client (LOWER(company_name)) WHERE is_archived = FALSE AND deleted_at IS NULL; -CREATE UNIQUE INDEX uq_client_siren_active - ON client (siren) - WHERE siren IS NOT NULL AND is_archived = FALSE AND deleted_at IS NULL; - -CREATE UNIQUE INDEX uq_client_email_active - ON client (LOWER(email)) - WHERE is_archived = FALSE AND deleted_at IS NULL; - -- ===================================================================== -- Jointure M2M client ↔ category -- ===================================================================== @@ -772,7 +766,7 @@ class Client implements TimestampableInterface, BlamableInterface - **Réponse 201** : le client créé avec son `id`. Le front enchaîne ensuite les PATCH par onglet. - **Codes** : - `201` / `400` / `401` / `403` - - `409 Conflict` si doublon (companyName, siren, ou email — RG-1.15 / RG-1.16 / RG-1.17) + - `409 Conflict` si doublon de nom de société (`companyName` — RG-1.16). SIREN et email ne sont pas uniques (cf. Q4, § 2.4). - `422 Unprocessable Entity` : - RG-1.01 : ni firstName ni lastName - RG-1.03 : distributor + broker remplis simultanément @@ -912,9 +906,9 @@ Cf. § 2.6. Pattern Shared standard. ### Unicité -- **RG-1.15** : Le `siren` est unique parmi les clients non archivés ET non soft-deletés (index partiel `uq_client_siren_active`). Tentative de doublon → 409 avec message `"Un client avec le SIREN \"{siren}\" existe déjà."`. -- **RG-1.16** : Le `companyName` est unique (case-insensitive) parmi les clients non archivés ET non soft-deletés (index partiel `uq_client_company_name_active`). Doublon → 409. -- **RG-1.17** : L'`email` principal est unique (case-insensitive) parmi les clients non archivés ET non soft-deletés (index partiel `uq_client_email_active`). Doublon → 409. +- **RG-1.15** : ~~Unicité SIREN~~ — **supprimée (décision Q4, 29/05/2026)**. Le `siren` n'est plus contraint unique : un même SIREN peut être partagé (établissements multiples). Pas d'index `uq_client_siren_active`. +- **RG-1.16** : Le `companyName` est unique (case-insensitive) parmi les clients non archivés ET non soft-deletés (index partiel `uq_client_company_name_active`). Doublon → 409 avec message `"Un client nommé \"{companyName}\" existe déjà."`. **Seule unicité métier conservée (Q4).** +- **RG-1.17** : ~~Unicité email principal~~ — **supprimée (décision Q4, 29/05/2026)**. L'`email` principal n'est plus contraint unique (un même email peut servir plusieurs clients). Pas d'index `uq_client_email_active`. ### Normalisation serveur (formatage) @@ -963,7 +957,7 @@ Cf. § 2.6. Pattern Shared standard. - [ ] **RG-1.12** : POST onglet Comptabilité avec paymentType=VIREMENT sans bank → 422 - [ ] **RG-1.13** : POST onglet Comptabilité paymentType=LCR sans RIB → 422 ; DELETE du dernier RIB en LCR → 409 - [ ] **RG-1.14** : front-driven uniquement, pas de test back -- [ ] **RG-1.15/16/17** : POST avec SIREN/companyName/email déjà pris → 409 ; POST avec même SIREN/companyName/email après archivage → 201 +- [ ] **RG-1.16** : POST avec `companyName` déjà pris → 409 ; POST avec même `companyName` après archivage de l'ancien → 201. SIREN et email dupliqués → 201 (plus d'unicité — RG-1.15/1.17 supprimées, Q4). - [ ] **RG-1.18** : POST `companyName="acme sas"` → BDD persiste `"ACME SAS"` - [ ] **RG-1.19** : POST `firstName="JEAN"`, `lastName="dupont"` → persiste `"Jean"`, `"Dupont"` - [ ] **RG-1.20** : POST `phonePrimary="06.12.34.56.78"` → persiste `"0612345678"` @@ -977,7 +971,7 @@ Cf. § 2.6. Pattern Shared standard. - [ ] **Compta POST création** : Compta → 403 (pas de `manage` global) - [ ] **PATCH mix groupes** : Bureau envoie payload avec `companyName` (write:main) + `siren` (write:accounting) → **403 sur tout le payload** (strict, RG-1.28) - [ ] **Audit** : POST + PATCH + archive → audit_log avec entity_type='Client', `changes` correct ; **iban/bic présents dans le diff** (pas d'AuditIgnore, cf. § 6.1) -- [ ] **Migration** : `make db-reset` → schéma OK, seed des 4 référentiels + CategoryType (DISTRIBUTEUR/COURTIER/SECTEUR/AUTRE) présent ; index partiels présents +- [ ] **Migration** : `make db-reset` → schéma OK, seed des 4 référentiels + CategoryType (DISTRIBUTEUR/COURTIER/SECTEUR/AUTRE) présent ; index partiel unique `uq_client_company_name_active` présent (un seul — cf. Q4) ### 8.2 Cas à couvrir (front — Vitest) diff --git a/docs/specs/M1-clients/spec-front.md b/docs/specs/M1-clients/spec-front.md index b6384b9..4b627e0 100644 --- a/docs/specs/M1-clients/spec-front.md +++ b/docs/specs/M1-clients/spec-front.md @@ -171,13 +171,13 @@ Saisir une ou plusieurs adresses du client, rattachées à un ou plusieurs sites ### Onglet « Comptabilité » -⚠ **Accessible uniquement aux rôles avec `commercial.clients.accounting.manage`** (Admin seul au M1). Bureau et Commerciale ne voient pas l'onglet. Compta voit l'onglet **en lecture seule** (cf. décision Compta lecture seule). +⚠ **Accessible aux rôles avec `commercial.clients.accounting.manage`** (Admin + Compta au M1). Bureau et Commerciale ne voient pas l'onglet. **Compta peut éditer cet onglet** (champs SIREN / N° compte / TVA / Délai / Type de règlement / Banque / RIBs) — cf. décision Q1, aligné docx. Compta ne peut pas créer un client (pas de `manage` général). **Champs comptables** : | Champ | Type | Obligatoire | Règle | |---|---|---|---| -| **SIREN** | `` (masque 9 chiffres) | Oui | RG-1.15 (unicité) | +| **SIREN** | `` (masque 9 chiffres) | Oui | Format 9 chiffres. **Pas d'unicité** (décision Q4) | | **Numéro de compte** | `` | Oui | — | | **Mode de TVA** | `` | Oui | Liste depuis `/api/tva_modes` | | **N° de TVA** | `` | Oui | — | @@ -274,7 +274,7 @@ Le composant `Code postal` + `Ville` + `Adresse` est branché sur **api-adresse. | 4 | Workflow par onglet | **Sauvegarde incrémentale**. POST formulaire principal crée le `Client` (status implicite « actif »). Chaque onglet validé = PATCH partiel par groupe de sérialisation dédié. Pas d'état « draft ». | | 5 | Onglets « À venir » | **Placeholders blancs** (frames vides, pas de message). Ré-activables sans rebuild quand les modules associés arriveront. | | 6 | Archive vs soft delete | **Flag `is_archived` séparé de `deleted_at`**. Archive ≠ delete : un client archivé est masqué par défaut mais reste en BDD éditable (Admin seul). Filtres UI distincts. Soft delete = HP M2. | -| 7 | Unicité métier | **SIREN, Nom entreprise, Email principal — tous uniques parmi non-archivés.** Index partiels Postgres. Tentative de doublon → 409 Conflict. | +| 7 | Unicité métier | **Nom d'entreprise uniquement** (case-insensitive, parmi non-archivés) — décision Q4. SIREN et email NON uniques. Index partiel Postgres `uq_client_company_name_active`. Doublon de nom → 409 Conflict. | | 8 | Téléphones (max 2) | **2 colonnes plates** `phone_primary` + `phone_secondary`. Pas de table séparée. | | 9 | API code postal | **api-adresse.data.gouv.fr** (BAN). Appel direct front via composable dédié. Cas dégradé : saisie libre + toast. | | 10 | Référentiels comptables | **4 entités CRUD-ables** (`TvaMode`, `PaymentDelay`, `PaymentType`, `Bank`) seedées au M1, CRUD admin futur (HP-M2). |