docs(commercial) : Q4 unicité limitée au nom de société + fix onglet compta
This commit is contained in:
@@ -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.
|
- 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).
|
- 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
|
Index unique partiel (`WHERE is_archived = false AND deleted_at IS NULL`) sur :
|
||||||
2. `siren` — unicité du SIREN
|
|
||||||
3. `LOWER(email)` — unicité de l'email principal
|
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`.
|
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_created_by ON client(created_by);
|
||||||
CREATE INDEX idx_client_updated_by ON client(updated_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
|
CREATE UNIQUE INDEX uq_client_company_name_active
|
||||||
ON client (LOWER(company_name))
|
ON client (LOWER(company_name))
|
||||||
WHERE is_archived = FALSE AND deleted_at IS NULL;
|
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
|
-- 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.
|
- **Réponse 201** : le client créé avec son `id`. Le front enchaîne ensuite les PATCH par onglet.
|
||||||
- **Codes** :
|
- **Codes** :
|
||||||
- `201` / `400` / `401` / `403`
|
- `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` :
|
- `422 Unprocessable Entity` :
|
||||||
- RG-1.01 : ni firstName ni lastName
|
- RG-1.01 : ni firstName ni lastName
|
||||||
- RG-1.03 : distributor + broker remplis simultanément
|
- RG-1.03 : distributor + broker remplis simultanément
|
||||||
@@ -912,9 +906,9 @@ Cf. § 2.6. Pattern Shared standard.
|
|||||||
|
|
||||||
### Unicité
|
### 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.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.
|
- **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** : 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.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)
|
### 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.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.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.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.18** : POST `companyName="acme sas"` → BDD persiste `"ACME SAS"`
|
||||||
- [ ] **RG-1.19** : POST `firstName="JEAN"`, `lastName="dupont"` → persiste `"Jean"`, `"Dupont"`
|
- [ ] **RG-1.19** : POST `firstName="JEAN"`, `lastName="dupont"` → persiste `"Jean"`, `"Dupont"`
|
||||||
- [ ] **RG-1.20** : POST `phonePrimary="06.12.34.56.78"` → persiste `"0612345678"`
|
- [ ] **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)
|
- [ ] **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)
|
- [ ] **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)
|
- [ ] **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)
|
### 8.2 Cas à couvrir (front — Vitest)
|
||||||
|
|
||||||
|
|||||||
@@ -171,13 +171,13 @@ Saisir une ou plusieurs adresses du client, rattachées à un ou plusieurs sites
|
|||||||
|
|
||||||
### Onglet « Comptabilité »
|
### 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** :
|
**Champs comptables** :
|
||||||
|
|
||||||
| Champ | Type | Obligatoire | Règle |
|
| Champ | Type | Obligatoire | Règle |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **SIREN** | `<MalioInputText>` (masque 9 chiffres) | Oui | RG-1.15 (unicité) |
|
| **SIREN** | `<MalioInputText>` (masque 9 chiffres) | Oui | Format 9 chiffres. **Pas d'unicité** (décision Q4) |
|
||||||
| **Numéro de compte** | `<MalioInputText>` | Oui | — |
|
| **Numéro de compte** | `<MalioInputText>` | Oui | — |
|
||||||
| **Mode de TVA** | `<MalioSelect>` | Oui | Liste depuis `/api/tva_modes` |
|
| **Mode de TVA** | `<MalioSelect>` | Oui | Liste depuis `/api/tva_modes` |
|
||||||
| **N° de TVA** | `<MalioInputText>` | Oui | — |
|
| **N° de TVA** | `<MalioInputText>` | 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 ». |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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). |
|
| 10 | Référentiels comptables | **4 entités CRUD-ables** (`TvaMode`, `PaymentDelay`, `PaymentType`, `Bank`) seedées au M1, CRUD admin futur (HP-M2). |
|
||||||
|
|||||||
Reference in New Issue
Block a user