[ERP-78] Refonte taxonomie Catégories : type unique CLIENT + Category.code + RG-1.03/1.29 par code (#42)
Auto Tag Develop / tag (push) Successful in 8s
Auto Tag Develop / tag (push) Successful in 8s
Refonte de la taxonomie Catégories (décision produit 01/06) : le modèle est inversé. ## Modèle - **UN SEUL `category_type` : CLIENT**. `Distributeur` / `Courtier` / `Secteur` / `Autre` (+ catégories métier) deviennent des `Category` rattachées à CLIENT. - Filtrage métier sur un **`code` stable porté par `Category`** (NOT NULL, unique partiel `uq_category_code`), slug MAJUSCULE auto-généré du nom (`CategoryCodeGenerator`), figé à la création, exposé en **lecture seule**. ## Contenu - **M0** : `Category.code` (entité + migration corrective `Version20260602100000` au namespace racine + `COMMENT ON COLUMN` + catalogue + ligne `test-db-setup`). Retrofit `Version20260528120000` rendu conscient des colonnes. - **Seed** : type unique CLIENT, catégories codées (`Distributeur→DISTRIBUTEUR`, etc.), anciens types supprimés. Fixtures `CategoryType`/`Category`/`Client` alignées. - **RG-1.03** : `ClientProcessor::hasCategoryCode` — un distributor/broker doit porter la `Category` de code `DISTRIBUTEUR`/`COURTIER`. Filtre liste/export `categoryType` → `categoryCode`. - **RG-1.29** : `ClientAddress::validateCategoryCodes` — denylist des codes `DISTRIBUTEUR`/`COURTIER` sur une adresse (toute autre catégorie autorisée). - **Specs** M0/M1 (back + front) amendées. ## Tests `make php-cs-fixer-allow-risky` OK ; `make db-reset` OK (type unique CLIENT + 11 catégories codées, idempotent) ; `make test` **443 vert**. Ajouts : RG-1.03 courtier, génération/unicité/lecture-seule du code (`CategoryCodeTest`). ## Coordination - #76 (#500) : RG-1.29 réécrite ici sur le nouveau modèle ; #76 ne garde que le gap 2 (mapping CHECK adresse → 422), indépendant de la taxonomie. - ERP-68 (#486) : fixtures démo (déjà mergées via #41) adaptées ici au type unique CLIENT + codes. - Front #480–483 : selects Catégorie / distributeur / courtier basés sur le `code` (`?categoryCode=`), plus le type. Décisions actées avec le PO : `code` NOT NULL auto-généré (slug) ; périmètre complet (réécriture RG + fixtures déjà mergées). --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #42 Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr> Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #42.
This commit is contained in:
@@ -465,26 +465,32 @@ CREATE TABLE client_rib (
|
||||
CREATE INDEX idx_client_rib_client ON client_rib(client_id);
|
||||
```
|
||||
|
||||
### 3.3 Seed `CategoryType` (extension du M0)
|
||||
### 3.3 Seed taxonomie — type unique `CLIENT` + `Category.code` (refonte ERP-78)
|
||||
|
||||
Au M0, la table `category_type` a été créée mais reste vide (HP-1 du M0). Le M1 lève cette restriction avec un seed initial des **types métier** dont le module Tiers a besoin :
|
||||
> **⚠ Refonte ERP-78 (décision produit 01/06) — le modèle ci-dessous remplace l'ancien.**
|
||||
> Historique : à l'origine (#38), `DISTRIBUTEUR` / `COURTIER` / `SECTEUR` / `AUTRE` étaient des **`category_type`**. Le modèle a été **inversé** :
|
||||
>
|
||||
> - **UN SEUL `category_type` : `CLIENT`** (code `CLIENT`, label « Client »).
|
||||
> - `Distributeur` / `Courtier` / `Secteur` / `Autre` (+ catégories métier fines) sont désormais des **`Category`** rattachées au type `CLIENT`.
|
||||
> - Le filtrage métier ne se fait plus sur le **type** mais sur un **`code` stable porté par la `Category`** (NOT NULL, unique parmi les actifs — index partiel `uq_category_code`). Le code est un **slug MAJUSCULE auto-généré du nom** (`CategoryCodeGenerator`), figé à la création, et exposé en **lecture seule** (groupe `category:read`). Les codes `DISTRIBUTEUR` / `COURTIER` (anciennement portés par le type) sont reportés sur les `Category` correspondantes.
|
||||
|
||||
Seed cible (migration corrective `Version20260602100000`, namespace racine) :
|
||||
|
||||
```sql
|
||||
INSERT INTO category_type (code, label, position) VALUES
|
||||
('DISTRIBUTEUR', 'Distributeur', 10),
|
||||
('COURTIER', 'Courtier', 20),
|
||||
('SECTEUR', 'Secteur', 30),
|
||||
('AUTRE', 'Autre', 99);
|
||||
-- Type unique
|
||||
INSERT INTO category_type (code, label) VALUES ('CLIENT', 'Client') ON CONFLICT (code) DO NOTHING;
|
||||
-- Catégories système sous CLIENT (codes stables pilotant les RG)
|
||||
-- Distributeur -> DISTRIBUTEUR, Courtier -> COURTIER, Secteur -> SECTEUR, Autre -> AUTRE
|
||||
```
|
||||
|
||||
> **Note** : le CRUD admin de `CategoryType` reste HP (cf. M0).
|
||||
> **Note** : le CRUD admin de `CategoryType` reste HP (cf. M0). Le `code` de `Category` n'est PAS saisissable via l'API (auto-généré côté serveur).
|
||||
>
|
||||
> **Seed en DEUX endroits (décision 29/05, vérifiée empiriquement)** : le `make db-reset` lance les fixtures, dont le purger Doctrine **vide `category_type`** (entité M0 mappée) avant `load()` → un seed posé uniquement en migration disparaît en dev/test. Donc :
|
||||
> 1. **Migration** (`ON CONFLICT (code) DO NOTHING`) → sert en **prod** (pas de fixtures).
|
||||
> 2. **Fixture Commercial idempotente** (ex. `CommercialReferentialFixtures`) re-seedant les 4 types → survit au `db-reset`, satisfait le critère « 4 types présents après db-reset ».
|
||||
> **Seed en DEUX endroits (décision 29/05, vérifiée empiriquement)** : le `make db-reset` lance les fixtures, dont le purger Doctrine **vide `category` / `category_type`** (entités M0 mappées) avant `load()` → un seed posé uniquement en migration disparaît en dev/test. Donc :
|
||||
> 1. **Migration** (`ON CONFLICT` / guards `NOT EXISTS`) → sert en **prod** (pas de fixtures).
|
||||
> 2. **Fixtures idempotentes** (`CategoryTypeFixtures` → type CLIENT ; `CategoryFixtures` → catégories codées sous CLIENT) → survivent au `db-reset`.
|
||||
>
|
||||
> ⚠ **À venir en ERP-54** : `tva_mode` / `payment_delay` / `payment_type` / `bank` ne sont pas encore des entités mappées au M1.0 → le purger ne les touche pas, leur seed migration survit. **Dès qu'ERP-54 crée leurs entités, ils seront purgés au db-reset** → il faudra les ajouter à la même fixture référentielle.
|
||||
> 🔗 **Coordination ERP-68** : ERP-53 pose la fixture référentielle minimale (4 category_types). ERP-68 l'**étend** (clients de démo, ~12-15) sans la dupliquer.
|
||||
> 🔗 **Coordination ERP-68** : ERP-78 (cette refonte) atterrit avant ERP-68. `CategoryFixtures` / `ClientFixtures` ont été adaptées au type unique CLIENT + codes (les tiers distributeur/courtier portent les `Category` de code DISTRIBUTEUR/COURTIER).
|
||||
|
||||
### 3.4 Entité `Client` — squelette
|
||||
|
||||
@@ -742,7 +748,7 @@ class Client implements TimestampableInterface, BlamableInterface
|
||||
- **Security** : `is_granted('commercial.clients.view')`
|
||||
- **Query params** :
|
||||
- `includeArchived=true|false` (default `false`)
|
||||
- `categoryType=<code>` (filtre par type de catégorie via `SearchFilter`)
|
||||
- `categoryCode=<code>` (filtre les clients ayant ≥ 1 `Category` de ce code stable — ERP-78 ; ex. `DISTRIBUTEUR`, `COURTIER`)
|
||||
- `search=<text>` (recherche fuzzy sur companyName + lastName + email)
|
||||
- **Tri par défaut** : `companyName ASC`
|
||||
- **Pagination** : front via `<MalioDataTable>` (volumétrie cible faible). Pas de pagination serveur au M1.
|
||||
@@ -881,7 +887,7 @@ Cf. § 2.6. Pattern Shared standard.
|
||||
|
||||
- **RG-1.01** : Au moins l'un des champs `firstName` (Prénom du contact principal) ou `lastName` (Nom du contact principal) doit être renseigné. Sinon → 422.
|
||||
- **RG-1.02** : Le champ `phoneSecondary` est optionnel et apparaît au clic sur un bouton `+` côté front. Maximum 2 téléphones (primary + secondary). Comportement purement front au niveau UI ; côté serveur, les 2 colonnes existent et sont distinctes.
|
||||
- **RG-1.03** : Les champs `distributor` et `broker` sont **mutuellement exclusifs** (au plus une seule des deux est renseignée). Tentative d'envoyer les deux → 422. Contrainte CHECK en base également : `NOT (distributor_id IS NOT NULL AND broker_id IS NOT NULL)`. La liste front de `distributor` = clients ayant au moins une catégorie de type `DISTRIBUTEUR` ; idem pour `broker` avec `COURTIER`.
|
||||
- **RG-1.03** : Les champs `distributor` et `broker` sont **mutuellement exclusifs** (au plus une seule des deux est renseignée). Tentative d'envoyer les deux → 422. Contrainte CHECK en base également : `NOT (distributor_id IS NOT NULL AND broker_id IS NOT NULL)`. Un `distributor` référencé doit porter une **`Category` de code `DISTRIBUTEUR`** ; un `broker` une **`Category` de code `COURTIER`** — sinon 422. _(Refonte ERP-78 : le filtrage se fait sur le `code` de la `Category`, plus sur le type — `ClientProcessor::hasCategoryCode`.)_ La liste front de `distributor` = clients ayant une catégorie de code `DISTRIBUTEUR`, via `GET /api/clients?categoryCode=DISTRIBUTEUR` ; idem `broker` avec `COURTIER`.
|
||||
|
||||
### Onglet Information
|
||||
|
||||
@@ -946,9 +952,9 @@ Cf. § 2.6. Pattern Shared standard.
|
||||
|
||||
- **RG-1.28** : Si un PATCH contient des champs de **plusieurs groupes** de sérialisation et que l'utilisateur **n'a pas toutes les permissions** correspondantes, le `ClientProcessor` renvoie **403 Forbidden sur l'ensemble du payload** (mode strict — pas de filtrage silencieux). Le front est responsable de ne JAMAIS envoyer de champs hors-permission (les onglets masqués via `usePermissions()` ne génèrent pas de payload). Cette règle protège contre les appels API directs malveillants. Exemple : un Bureau qui envoie `{ "companyName": "...", "siren": "..." }` → 403, le message d'erreur précise « Champ `siren` requiert la permission `commercial.clients.accounting.manage` ».
|
||||
|
||||
### Catégorie sur ClientAddress (filtrage par type)
|
||||
### Catégorie sur ClientAddress (filtrage par code)
|
||||
|
||||
- **RG-1.29** : Le `<MalioSelectCheckbox>` Catégorie de l'onglet Adresse n'expose **que** les `Category` dont `categoryType.code IN ('SECTEUR', 'AUTRE')`. Les types `DISTRIBUTEUR` et `COURTIER` qualifient une **relation entre clients** (cf. RG-1.03) et n'ont pas de sens sur une adresse physique. Implémentation : `ClientAddressProvider` filtre côté serveur via paramètre de requête à l'endpoint `GET /api/categories?categoryType.code[]=SECTEUR&categoryType.code[]=AUTRE` (SearchFilter API Platform). Côté validation du POST/PATCH : si l'utilisateur tente de poster une catégorie de type DISTRIBUTEUR ou COURTIER sur une adresse → **422** avec violation `categories: "Type de catégorie non autorisé sur une adresse."`.
|
||||
- **RG-1.29** _(refonte ERP-78)_ : sur une adresse, les `Category` de **code `DISTRIBUTEUR` ou `COURTIER`** sont **interdites** — elles qualifient une **relation entre clients** (cf. RG-1.03) et n'ont pas de sens sur une adresse physique. **Toute autre** catégorie (type unique CLIENT) est autorisée. Validation du POST/PATCH : poster une catégorie de code DISTRIBUTEUR/COURTIER sur une adresse → **422** avec violation `categories: "Type de catégorie non autorisé sur une adresse."` (`ClientAddress::validateCategoryCodes`). Côté front, le `<MalioSelectCheckbox>` Catégorie de l'onglet Adresse exclut les `Category` de code `DISTRIBUTEUR` / `COURTIER` (le `code` est exposé en lecture sur `/api/categories`).
|
||||
|
||||
## 8. Tests à automatiser
|
||||
|
||||
@@ -957,7 +963,7 @@ Cf. § 2.6. Pattern Shared standard.
|
||||
- [ ] **RG-1.01** : POST sans firstName ni lastName → 422
|
||||
- [ ] **RG-1.02** : POST avec phoneSecondary rempli → persistance OK ; PATCH ajoutant un 3e téléphone → côté API, 2 colonnes uniquement (test que le payload ne peut pas créer un 3e)
|
||||
- [ ] **RG-1.03** : POST avec distributor ET broker → 422 ; POST distributor seul → 201
|
||||
- [ ] **RG-1.03** : POST distributor référençant un client SANS catégorie de type DISTRIBUTEUR → 422 (validation custom)
|
||||
- [ ] **RG-1.03** : POST distributor référençant un client SANS catégorie de code DISTRIBUTEUR → 422 (validation custom `ClientProcessor::hasCategoryCode`)
|
||||
- [ ] **RG-1.04** : PATCH onglet Information par un user Commerciale avec champs incomplets → 422 ; même PATCH par Admin → 200
|
||||
- [ ] **RG-1.05** : POST contact sans firstName ni lastName → 422 (BDD CHECK lève une exception)
|
||||
- [ ] **RG-1.06/07/08** : POST adresse avec isProspect=true ET isDelivery=true → 422 / CHECK
|
||||
|
||||
Reference in New Issue
Block a user