diff --git a/README.md b/README.md index 5388528..b01b52c 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ Disponibles uniquement après `make db-reset` / `make fixtures` (état « avec s | `bob` | `bob` | ROLE_USER | — | | `bureau` | `demo` | ROLE_USER | clients : view + manage | | `compta` | `demo` | ROLE_USER | clients : view + accounting.view / manage | -| `commerciale` | `demo` | ROLE_USER | clients : view + manage (Information obligatoire — RG-1.04) | +| `commerciale` | `demo` | ROLE_USER | clients : view + manage | | `usine` | `demo` | ROLE_USER | aucun accès clients | --- diff --git a/docs/specs/M1-clients/cahier-test-back-M1.md b/docs/specs/M1-clients/cahier-test-back-M1.md index 33fe309..f0b58e4 100644 --- a/docs/specs/M1-clients/cahier-test-back-M1.md +++ b/docs/specs/M1-clients/cahier-test-back-M1.md @@ -10,8 +10,8 @@ trous, zéro duplication »). ERP-60 n'écrit QUE les tests des RG non déjà couvertes par la stack, et mappe ici l'intégralité des RG (existantes + nouvelles + déléguées). Les tests dépendants -des **rôles métier** (matrice RBAC bureau/compta/commerciale/usine + RG-1.04 -fonctionnel) sont **délégués à ERP-74 (#493)** : ces rôles n'existent qu'après le +des **rôles métier** (matrice RBAC bureau/compta/commerciale/usine) sont +**délégués à ERP-74 (#493)** : ces rôles n'existent qu'après le merge de la stack. ## Mapping RG → test @@ -21,7 +21,7 @@ merge de la stack. | ~~RG-1.01~~ | _(supprimée V1 — refonte-contact)_ contact inline retiré du Client ; complétude couverte par RG-1.05 / RG-1.14 (`ClientContact`) | — | refonte-contact | | ~~RG-1.02~~ | _(supprimée du Client V1)_ téléphones inline retirés du Client (testés sur `ClientContact`) | — | refonte-contact | | RG-1.03 | distributor/broker exclusifs + type catégorie | `ClientApiTest::testPostWithDistributorAndBrokerReturns422` ; `::testPostDistributorReferencingNonDistributorReturns422` ; `::testPostValidDistributorReturns201` ; `ClientProcessorTest` (unit) | ERP-55 | -| RG-1.04 | Onglet Information obligatoire pour rôle Commerciale | `ClientProcessorTest::testCommercialeIncompleteInformationIsUnprocessable` ; `::testNonCommercialeSkipsInformationCompleteness` (unit, dormant). **Test fonctionnel + durcissement → ERP-74** | ERP-55 / **ERP-74** | +| ~~RG-1.04~~ | _(SUPPRIMÉE)_ Onglet Information désormais facultatif pour tous les rôles (validation de complétude retirée) | — | ERP-55 / ERP-74 | | RG-1.05 | Contact : prénom OU nom → 422 (CHECK) | `ClientSubResourceApiTest::testPostContactWithoutNameReturns422` | ERP-57 | | RG-1.06/07/08 | Adresse prospect exclusive de livraison/facturation → 422 (Assert\Callback + CHECK filet) | `ClientAddressTest::testProspectAddressCannotBeDelivery` ; `::testProspectAddressCannotBeBilling` | ERP-60 / **ERP-76** | | RG-1.09 | Code postal `^[0-9]{4,5}$` → 422 | `ClientSubResourceApiTest::testPostAddressWithInvalidPostalCodeReturns422` | ERP-57 | @@ -60,8 +60,7 @@ merge de la stack. - **Matrice RBAC différenciée** par rôle métier (Bureau / Compta / Commerciale / Usine) : 200/403 par verbe et par onglet selon le rôle. -- **RG-1.04 fonctionnel** : PATCH onglet Information par une Commerciale avec - champs incomplets → 422 ; même PATCH par Admin → 200 (+ durcissement code/spec). +- ~~**RG-1.04 fonctionnel**~~ : _(SUPPRIMÉE)_ l'onglet Information est désormais facultatif pour tous les rôles. - Raison : ces rôles métier ne sont seedés qu'après le merge de la stack M1. ## Gaps & suivi diff --git a/docs/specs/M1-clients/spec-back.md b/docs/specs/M1-clients/spec-back.md index fb30551..c5706d2 100644 --- a/docs/specs/M1-clients/spec-back.md +++ b/docs/specs/M1-clients/spec-back.md @@ -310,7 +310,7 @@ CREATE TABLE client ( distributor_id INT REFERENCES client(id) ON DELETE SET NULL, broker_id INT REFERENCES client(id) ON DELETE SET NULL, triage_service BOOLEAN NOT NULL DEFAULT FALSE, - -- Onglet Information (Commerciale obligatoire — RG-1.04 — null sinon) + -- Onglet Information (facultatif pour tous — RG-1.04 supprimée) description TEXT, competitors VARCHAR(255), founded_at DATE, @@ -864,8 +864,7 @@ Cf. § 2.6. Pattern Shared standard. ### Onglet Information -- **RG-1.04** _(durcie — ERP-74)_ : Pour un utilisateur portant le rôle métier **Commerciale**, **tous** les champs de l'onglet Information (`description`, `competitors`, `foundedAt`, `employeesCount`, `revenueAmount`, `directorName`, `profitAmount`) sont obligatoires sur **POST et sur tout PATCH**, **indépendamment des champs réellement envoyés** (la condition d'intersection avec `client:write:information` a été retirée). Pour les autres rôles, ces champs restent optionnels. Implémenté via un validator custom `ClientInformationCompletenessValidator` invoqué systématiquement par le `ClientProcessor` quand le user porte le rôle Commerciale. - - **Conséquence** : le POST n'exposant que le groupe `client:write:main`, l'onglet Information n'y est pas renseignable → une Commerciale obtient **422** sur tout POST (cf. § 8.1). La complétude se fait donc via les PATCH `client:write:information` ultérieurs. Un Admin (non gaté) crée normalement (201). +- ~~**RG-1.04**~~ _(SUPPRIMÉE)_ : l'onglet Information est désormais **facultatif pour tous les rôles**, y compris Commerciale. Les champs `description`, `competitors`, `foundedAt`, `employeesCount`, `revenueAmount`, `directorName`, `profitAmount` restent optionnels (colonnes nullable, aucune validation de complétude). Le validator `ClientInformationCompletenessValidator` et le gating par rôle métier dans le `ClientProcessor` ont été retirés. ### Onglet Contact @@ -883,7 +882,7 @@ Cf. § 2.6. Pattern Shared standard. ### Onglet Comptabilité -- **RG-1.30** _(ajoutée — correctif incohérence spec-front/spec-back)_ : à la **validation complète de l'onglet Comptabilité**, les six champs scalaires `siren`, `accountNumber`, `tvaMode`, `nTva`, `paymentDelay`, `paymentType` sont **obligatoires** (alignement sur spec-front § Onglet Comptabilité). Colonnes `nullable` en base (l'onglet est rempli dans un second temps, et l'onglet principal ne les envoie pas) + validateur contextuel `ClientAccountingCompletenessValidator` invoqué par le `ClientProcessor` — même parti que RG-1.04 (Information). Déclenchement : uniquement quand **les six champs sont présents dans le payload** (le front les envoie toujours ensemble via « Valider ») ; un PATCH ciblant un sous-ensemble de champs comptables (édition ponctuelle) n'est pas soumis à la complétude. Chaque champ manquant → 422 sur son `propertyPath` (mapping inline front, ERP-101). `bank` reste hors complétude (conditionnel RG-1.12). +- **RG-1.30** _(ajoutée — correctif incohérence spec-front/spec-back)_ : à la **validation complète de l'onglet Comptabilité**, les six champs scalaires `siren`, `accountNumber`, `tvaMode`, `nTva`, `paymentDelay`, `paymentType` sont **obligatoires** (alignement sur spec-front § Onglet Comptabilité). Colonnes `nullable` en base (l'onglet est rempli dans un second temps, et l'onglet principal ne les envoie pas) + validateur contextuel `ClientAccountingCompletenessValidator` invoqué par le `ClientProcessor` (parti pris : nullable + validateur d'onglet plutôt qu'un `Assert\NotBlank` sur l'entité). Déclenchement : uniquement quand **les six champs sont présents dans le payload** (le front les envoie toujours ensemble via « Valider ») ; un PATCH ciblant un sous-ensemble de champs comptables (édition ponctuelle) n'est pas soumis à la complétude. Chaque champ manquant → 422 sur son `propertyPath` (mapping inline front, ERP-101). `bank` reste hors complétude (conditionnel RG-1.12). - **RG-1.12** : Le champ `bank` est visible et obligatoire **uniquement** si `paymentType.code = 'VIREMENT'`. Validation server-side dans le `ClientProcessor` : si `payment_type.code = VIREMENT` et `bank IS NULL` → 422. - **RG-1.13** : Les champs RIB (`label`, `bic`, `iban`) sont obligatoires si **au moins un bloc RIB est présent ET** `paymentType.code = 'LCR'`. C'est-à-dire : - Si `paymentType.code = LCR` ET `client.ribs.count() = 0` → 422 « Au moins un RIB est obligatoire pour le type LCR ». @@ -938,7 +937,7 @@ Cf. § 2.6. Pattern Shared standard. - [ ] ~~RG-1.02~~ _(supprimée du Client V1)_ : plus de téléphones inline sur le Client (téléphones testés sur `ClientContact`) - [ ] **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 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 +- [x] ~~**RG-1.04**~~ _(SUPPRIMÉE)_ : onglet Information facultatif pour tous les rôles (plus de validation de complétude) - [ ] **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 - [ ] **RG-1.09** : POST adresse avec postalCode invalide (3 chiffres) → 422 ; CP/ville incohérents → 200 (pas de validation stricte côté serveur) diff --git a/docs/specs/M1-clients/spec-front.md b/docs/specs/M1-clients/spec-front.md index 88a19ab..466d020 100644 --- a/docs/specs/M1-clients/spec-front.md +++ b/docs/specs/M1-clients/spec-front.md @@ -13,7 +13,7 @@ date_redaction: 2026-05-28 # === LIENS === maquette_figma: "https://www.figma.com/design/jRYgT0T9c03VsEbjGhCwwS/Composants---Design-System?node-id=1132-31898" -regles_metier: [RG-1.01, RG-1.02, RG-1.03, RG-1.04, RG-1.05, RG-1.06, RG-1.07, RG-1.08, RG-1.09, RG-1.10, RG-1.11, RG-1.12, RG-1.13, RG-1.14, RG-1.15, RG-1.16, RG-1.17, RG-1.18, RG-1.19, RG-1.20, RG-1.21, RG-1.22, RG-1.23, RG-1.24, RG-1.25, RG-1.26, RG-1.27, RG-1.28, RG-1.29] +regles_metier: [RG-1.01, RG-1.02, RG-1.03, RG-1.05, RG-1.06, RG-1.07, RG-1.08, RG-1.09, RG-1.10, RG-1.11, RG-1.12, RG-1.13, RG-1.14, RG-1.15, RG-1.16, RG-1.17, RG-1.18, RG-1.19, RG-1.20, RG-1.21, RG-1.22, RG-1.23, RG-1.24, RG-1.25, RG-1.26, RG-1.27, RG-1.28, RG-1.29] roles: [Admin, Bureau, Compta, Commerciale, Usine] lien_spec_back: ./spec-back.md @@ -105,13 +105,13 @@ Saisir les informations de l'entreprise. | Champ | Type | Obligatoire | Règle | |---|---|---|---| -| **Description** | `` | Conditionnel | RG-1.04 (obligatoire pour rôle Commerciale) | -| **Concurrents** | `` | Conditionnel | RG-1.04 | -| **Date de création** (de l'entreprise) | `` (exception Malio — pas de composant date couvert) | Conditionnel | RG-1.04 | -| **Nombre de salariés** | `` | Conditionnel | RG-1.04 | -| **CA €** | `` | Conditionnel | RG-1.04 | -| **Dirigeant** | `` | Conditionnel | RG-1.04 | -| **Résultat €** | `` | Conditionnel | RG-1.04 | +| **Description** | `` | Non | — _(onglet facultatif, RG-1.04 supprimée)_ | +| **Concurrents** | `` | Non | — | +| **Date de création** (de l'entreprise) | `` (exception Malio — pas de composant date couvert) | Non | — | +| **Nombre de salariés** | `` | Non | — | +| **CA €** | `` | Non | — | +| **Dirigeant** | `` | Non | — | +| **Résultat €** | `` | Non | — | **Action** : « Valider » → PATCH partiel `/api/clients/{id}` (groupe `client:write:information`). diff --git a/frontend/modules/commercial/utils/clientEdit.ts b/frontend/modules/commercial/utils/clientEdit.ts index 6b949d7..c7551f6 100644 --- a/frontend/modules/commercial/utils/clientEdit.ts +++ b/frontend/modules/commercial/utils/clientEdit.ts @@ -12,10 +12,8 @@ * * Ces helpers ne touchent ni a l'API ni a l'etat reactif. * - * NOTE RG-1.04 (Information obligatoire pour la Commerciale) : volontairement NON - * miroitee cote front (cf. clientFormRules.ts) — /api/me n'expose pas le code de - * role et Bureau partage les permissions de Commerciale. Le back l'applique de - * maniere fiable (422) ; on laisse remonter ce 422 en toast. + * NOTE : l'onglet Information est facultatif pour tous les roles (RG-1.04 + * « Information obligatoire pour la Commerciale » retiree cote back). */ import { diff --git a/frontend/modules/commercial/utils/clientFormRules.ts b/frontend/modules/commercial/utils/clientFormRules.ts index 80ce4bf..a9ea3b5 100644 --- a/frontend/modules/commercial/utils/clientFormRules.ts +++ b/frontend/modules/commercial/utils/clientFormRules.ts @@ -9,12 +9,9 @@ * Le back reste la source de verite (les RG sont re-validees serveur) ; ces * regles ne servent qu'au feedback UI immediat (gating de boutons, visibilite). * - * NOTE RG-1.04 (Information obligatoire pour la Commerciale) : volontairement - * NON miroite cote front pour l'instant. Le payload /api/me ne porte pas le code - * de role (roles = IRIs opaques) et Bureau partage les memes permissions que - * Commerciale : aucun signal fiable pour distinguer le role cote front. Le back - * (ClientProcessor, via BusinessRoleAware) applique la regle de maniere fiable ; - * a rebrancher ici des qu'un code de role sera expose dans /api/me. + * NOTE : l'onglet Information est facultatif pour tous les roles. L'ancienne + * RG-1.04 (« Information obligatoire pour la Commerciale ») a ete retiree cote + * back — rien a miroiter ici. */ /** diff --git a/migrations/Version20260601000000.php b/migrations/Version20260601000000.php index 75a0934..0490f08 100644 --- a/migrations/Version20260601000000.php +++ b/migrations/Version20260601000000.php @@ -256,13 +256,13 @@ final class Version20260601000000 extends AbstractMigration $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', 'description', 'Onglet Information : description libre. Facultatif.'); + $this->comment('client', 'competitors', 'Onglet Information : concurrents identifies (texte libre ≤ 255). Facultatif.'); + $this->comment('client', 'founded_at', 'Onglet Information : date de creation de l entreprise. Facultatif.'); + $this->comment('client', 'employees_count', 'Onglet Information : effectif (entier >= 0). Facultatif.'); + $this->comment('client', 'revenue_amount', 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Facultatif.'); + $this->comment('client', 'director_name', 'Onglet Information : nom du dirigeant. Facultatif.'); + $this->comment('client', 'profit_amount', 'Onglet Information : resultat / benefice (NUMERIC 15,2). Facultatif.'); $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.'); diff --git a/src/Module/Commercial/Application/Validator/ClientAccountingCompletenessValidator.php b/src/Module/Commercial/Application/Validator/ClientAccountingCompletenessValidator.php index 32a2a23..7c4dc11 100644 --- a/src/Module/Commercial/Application/Validator/ClientAccountingCompletenessValidator.php +++ b/src/Module/Commercial/Application/Validator/ClientAccountingCompletenessValidator.php @@ -16,9 +16,9 @@ use Symfony\Component\Validator\ConstraintViolationList; * Type de reglement). La banque reste conditionnelle (RG-1.12) et les RIB aussi * (RG-1.13) : ils ne sont pas couverts ici. * - * Calque sur ClientInformationCompletenessValidator (RG-1.04) : colonnes nullable - * en base + validateur contextuel, plutot qu'un Assert\NotBlank sur l'entite (qui - * casserait le POST de l'onglet principal, lequel n'envoie aucun champ comptable). + * Parti pris : colonnes nullable en base + validateur contextuel, plutot qu'un + * Assert\NotBlank sur l'entite (qui casserait le POST de l'onglet principal, + * lequel n'envoie aucun champ comptable). * * Invoque par le ClientProcessor uniquement quand le payload porte les six champs * (= une validation d'onglet), jamais sur un PATCH ciblant un seul champ comptable. diff --git a/src/Module/Commercial/Application/Validator/ClientInformationCompletenessValidator.php b/src/Module/Commercial/Application/Validator/ClientInformationCompletenessValidator.php deleted file mode 100644 index fe374fe..0000000 --- a/src/Module/Commercial/Application/Validator/ClientInformationCompletenessValidator.php +++ /dev/null @@ -1,74 +0,0 @@ - valeur courante de l'onglet Information. - $fields = [ - 'description' => $client->getDescription(), - 'competitors' => $client->getCompetitors(), - 'foundedAt' => $client->getFoundedAt(), - 'employeesCount' => $client->getEmployeesCount(), - 'revenueAmount' => $client->getRevenueAmount(), - 'directorName' => $client->getDirectorName(), - 'profitAmount' => $client->getProfitAmount(), - ]; - - $violations = new ConstraintViolationList(); - - foreach ($fields as $property => $value) { - if ($this->isMissing($value)) { - $violations->add(new ConstraintViolation( - sprintf('Ce champ est obligatoire pour le role Commerciale (champ "%s").', $property), - null, - [], - $client, - $property, - $value, - )); - } - } - - if (count($violations) > 0) { - throw new ValidationException($violations); - } - } - - /** - * Une valeur est manquante si null ou, pour une chaine, vide apres trim. - * Les zeros numeriques (employeesCount = 0, profitAmount = "0.00") sont des - * valeurs valides : on ne les considere pas manquants. - */ - private function isMissing(mixed $value): bool - { - if (null === $value) { - return true; - } - - return is_string($value) && '' === trim($value); - } -} diff --git a/src/Module/Commercial/Infrastructure/ApiPlatform/State/Processor/ClientProcessor.php b/src/Module/Commercial/Infrastructure/ApiPlatform/State/Processor/ClientProcessor.php index e451ee7..8967c5b 100644 --- a/src/Module/Commercial/Infrastructure/ApiPlatform/State/Processor/ClientProcessor.php +++ b/src/Module/Commercial/Infrastructure/ApiPlatform/State/Processor/ClientProcessor.php @@ -9,11 +9,8 @@ use ApiPlatform\State\ProcessorInterface; use ApiPlatform\Validator\Exception\ValidationException; use App\Module\Commercial\Application\Service\ClientFieldNormalizer; use App\Module\Commercial\Application\Validator\ClientAccountingCompletenessValidator; -use App\Module\Commercial\Application\Validator\ClientInformationCompletenessValidator; use App\Module\Commercial\Domain\Entity\Client; -use App\Shared\Domain\Contract\BusinessRoleAwareInterface; use App\Shared\Domain\Contract\CategoryInterface; -use App\Shared\Domain\Security\BusinessRoles; use DateTimeImmutable; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\EntityManagerInterface; @@ -44,8 +41,8 @@ use Symfony\Component\Validator\ConstraintViolationList; * 2. Normalisation serveur (RG-1.18 a 1.21) via ClientFieldNormalizer. * 3. Regles metier : RG-1.03 (distributor/broker * exclusifs + type de categorie), RG-1.12 (Virement -> banque), - * RG-1.13 (LCR -> >= 1 RIB), RG-1.04 (completude Information exigee sur POST - * et tout PATCH pour le role Commerciale). + * RG-1.13 (LCR -> >= 1 RIB). L'onglet Information est entierement facultatif + * (RG-1.04 retiree : plus d'obligation, y compris pour le role Commerciale). * 4. Pose / retrait de archivedAt (RG-1.22 true=now, RG-1.23 false=null). * 5. Persistance via le persist_processor Doctrine, avec traduction des * collisions d'unicite en 409 (RG-1.16 doublon de nom ; RG-1.23 conflit de @@ -108,7 +105,6 @@ final class ClientProcessor implements ProcessorInterface #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private readonly ProcessorInterface $persistProcessor, private readonly ClientFieldNormalizer $normalizer, - private readonly ClientInformationCompletenessValidator $informationValidator, private readonly ClientAccountingCompletenessValidator $accountingValidator, private readonly Security $security, private readonly RequestStack $requestStack, @@ -136,7 +132,6 @@ final class ClientProcessor implements ProcessorInterface $this->validateDistributorBroker($data); $this->validateAccountingConsistency($data); $this->validateAccountingCompleteness($data); - $this->validateInformationCompleteness($data); try { return $this->persistProcessor->process($data, $operation, $uriVariables, $context); @@ -505,9 +500,9 @@ final class ClientProcessor implements ProcessorInterface * ce n'est pas une validation d'onglet (edition ponctuelle preservee). bank / * RIB restent geres par validateAccountingConsistency (RG-1.12 / RG-1.13). * - * Colonnes nullable en base + validateur contextuel (meme parti que RG-1.04) : - * un Assert\NotBlank sur l'entite casserait le POST de l'onglet principal, qui - * n'envoie aucun champ comptable. + * Colonnes nullable en base + validateur contextuel : un Assert\NotBlank sur + * l'entite casserait le POST de l'onglet principal, qui n'envoie aucun champ + * comptable. */ private function validateAccountingCompleteness(Client $data): void { @@ -520,21 +515,6 @@ final class ClientProcessor implements ProcessorInterface $this->accountingValidator->validate($data); } - /** - * RG-1.04 (durcie ERP-74) : si l'utilisateur porte le role metier - * Commerciale, TOUS les champs de l'onglet Information sont obligatoires sur - * POST comme sur TOUT PATCH — independamment des champs reellement envoyes - * (plus de condition d'intersection avec INFORMATION_FIELDS). Garantit qu'un - * client cree/edite par une Commerciale ne reste jamais avec un onglet - * Information incomplet. - */ - private function validateInformationCompleteness(Client $data): void - { - if ($this->currentUserIsCommerciale()) { - $this->informationValidator->validate($data); - } - } - /** * Vrai si au moins une categorie du client porte le code donne. S'appuie sur * CategoryInterface::getCode() (pas d'import de Category — regle ABSOLUE n°1). @@ -550,21 +530,12 @@ final class ClientProcessor implements ProcessorInterface return false; } - private function currentUserIsCommerciale(): bool - { - $user = $this->security->getUser(); - - return $user instanceof BusinessRoleAwareInterface - && $user->hasBusinessRole(BusinessRoles::COMMERCIALE); - } - /** * Cles ecrivables effectivement presentes dans le payload : on retire les * cles JSON-LD (@id, @context, @var...) et tout champ non rattache a un - * groupe d'ecriture connu. C'est la base du 422 d'archivage (RG-1.22) et du - * declenchement conditionnel de RG-1.04 — sans elles, un PATCH - * « representation complete » porteur de @id ferait croire a une - * modification multi-onglets. + * groupe d'ecriture connu. C'est la base du 422 d'archivage (RG-1.22) — + * sans elles, un PATCH « representation complete » porteur de @id ferait + * croire a une modification multi-onglets. * * @return list */ diff --git a/src/Module/Commercial/Infrastructure/DataFixtures/ClientFixtures.php b/src/Module/Commercial/Infrastructure/DataFixtures/ClientFixtures.php index 40a899f..2b741e2 100644 --- a/src/Module/Commercial/Infrastructure/DataFixtures/ClientFixtures.php +++ b/src/Module/Commercial/Infrastructure/DataFixtures/ClientFixtures.php @@ -234,7 +234,7 @@ class ClientFixtures extends Fixture implements DependentFixtureInterface $this->addAddress($services, ['Chatellerault'], '86100', 'Châtellerault', '15 rue du Conseil', isDelivery: true); } - // === Onglet Information complet (RG-1.04) === + // === Onglet Information complet (exemple de fiche renseignee) === [$holding, $isNew] = $this->ensureClient( $manager, companyName: 'Holding Premium Invest', diff --git a/src/Module/Core/Application/Rbac/RbacSeeder.php b/src/Module/Core/Application/Rbac/RbacSeeder.php index 6c6c327..ebbc0ca 100644 --- a/src/Module/Core/Application/Rbac/RbacSeeder.php +++ b/src/Module/Core/Application/Rbac/RbacSeeder.php @@ -36,8 +36,8 @@ final class RbacSeeder { /** * Codes des roles metier (snake_case, regex Role respectee). `commerciale` - * reference la constante Shared deja consommee par le ClientProcessor - * (RG-1.04) pour eviter tout drift : un seul litteral pour ce code. + * reference la constante Shared pour eviter tout drift : un seul litteral + * pour ce code. */ public const string ROLE_BUREAU = 'bureau'; public const string ROLE_COMPTA = 'compta'; diff --git a/src/Module/Core/Domain/Entity/User.php b/src/Module/Core/Domain/Entity/User.php index 09d0a85..74bd580 100644 --- a/src/Module/Core/Domain/Entity/User.php +++ b/src/Module/Core/Domain/Entity/User.php @@ -344,8 +344,8 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, Busines /** * Implemente BusinessRoleAwareInterface : vrai si l'un des roles RBAC * rattaches porte le code donne. Permet aux modules tiers de detecter un - * role metier (ex: `commerciale` pour RG-1.04 du M1 Clients) sans importer - * cette classe. Comparaison stricte sur Role::code. + * role metier (ex: `commerciale`) sans importer cette classe. Comparaison + * stricte sur Role::code. */ public function hasBusinessRole(string $roleCode): bool { diff --git a/src/Shared/Domain/Contract/BusinessRoleAwareInterface.php b/src/Shared/Domain/Contract/BusinessRoleAwareInterface.php index 92646a4..9f5fbb4 100644 --- a/src/Shared/Domain/Contract/BusinessRoleAwareInterface.php +++ b/src/Shared/Domain/Contract/BusinessRoleAwareInterface.php @@ -10,7 +10,6 @@ namespace App\Shared\Domain\Contract; * App\Shared\Domain\Security\BusinessRoles). * * Implementee par App\Module\Core\Domain\Entity\User. Permet a un module tiers - * (ex: Commercial — RG-1.04, completude Information pour le role Commerciale) * de raisonner sur les roles metier via Security::getUser() sans importer User * (regle ABSOLUE n°1 : pas d'import inter-modules). * diff --git a/src/Shared/Domain/Security/BusinessRoles.php b/src/Shared/Domain/Security/BusinessRoles.php index 83aac90..4270878 100644 --- a/src/Shared/Domain/Security/BusinessRoles.php +++ b/src/Shared/Domain/Security/BusinessRoles.php @@ -10,9 +10,11 @@ namespace App\Shared\Domain\Security; * Distincts des roles SYSTEME (cf. App\Module\Core\Domain\Security\SystemRoles : * `admin` / `user`). Un role metier porte une intention fonctionnelle (poste de * travail) et conditionne certaines regles de gestion au-dela des permissions - * RBAC pures — ex : RG-1.04 du M1 Clients rend l'onglet Information obligatoire - * pour le seul role Commerciale, alors que Commerciale et Bureau partagent les - * memes permissions (commercial.clients.view + manage, cf. spec-back M1 § 5.2). + * RBAC pures (Commerciale et Bureau partagent par ex. les memes permissions + * commercial.clients.view + manage mais peuvent porter des regles de gestion + * propres au poste). NB : l'ancienne RG-1.04 du M1 (« Information obligatoire + * pour Commerciale ») a ete retiree — l'onglet Information est facultatif pour + * tous ; la machinerie de role metier reste disponible pour de futures regles. * * Ces constantes vivent dans Shared (et non dans un module) pour que : * - le seed des roles cote Core (ERP-74) reference le meme code sans importer @@ -24,14 +26,14 @@ namespace App\Shared\Domain\Security; * Coordination stack M1 : * - ERP-74 seede le role `commerciale` avec ce code exact. * - ERP-59 / ERP-60 (declaration RBAC + tests personas) le reutilisent. - * - ERP-55 (ici) ne fait que le REFERENCER : tant qu'aucun user ne porte le - * role `commerciale`, la validation de completude Information reste dormante. + * - ERP-55 le referencait pour la completude Information (M1 RG-1.04), regle + * depuis retiree ; le code reste utilise par le seed RBAC et les personas. */ final class BusinessRoles { /** * Role metier « Commerciale » — code de Role RBAC (champ Role::code, - * snake_case impose par la regex Role). Conditionne RG-1.04. + * snake_case impose par la regex Role). */ public const string COMMERCIALE = 'commerciale'; diff --git a/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php b/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php index faa1188..70a32e2 100644 --- a/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php +++ b/src/Shared/Infrastructure/Database/ColumnCommentsCatalog.php @@ -175,13 +175,13 @@ final class ColumnCommentsCatalog '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).', + 'description' => 'Onglet Information : description libre. Facultatif.', + 'competitors' => 'Onglet Information : concurrents identifies (texte libre ≤ 255). Facultatif.', + 'founded_at' => 'Onglet Information : date de creation de l entreprise. Facultatif.', + 'employees_count' => 'Onglet Information : effectif (entier >= 0). Facultatif.', + 'revenue_amount' => 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Facultatif.', + 'director_name' => 'Onglet Information : nom du dirigeant. Facultatif.', + 'profit_amount' => 'Onglet Information : resultat / benefice (NUMERIC 15,2). Facultatif.', '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.', diff --git a/tests/Module/Commercial/Api/ClientApiTest.php b/tests/Module/Commercial/Api/ClientApiTest.php index 445447b..a590877 100644 --- a/tests/Module/Commercial/Api/ClientApiTest.php +++ b/tests/Module/Commercial/Api/ClientApiTest.php @@ -14,8 +14,8 @@ use App\Module\Sites\Domain\Entity\Site; * * Authentifies en ADMIN (bypass RBAC via isAdmin) : on valide ici les regles * METIER (normalisation, unicite, distributor/broker, archivage, liste). Le - * gating par permission (accounting.manage / archive / RG-1.28 strict, RG-1.04 - * Commerciale) est couvert par les tests unitaires du ClientProcessor : il + * gating par permission (accounting.manage / archive / RG-1.28 strict) est + * couvert par les tests unitaires du ClientProcessor : il * exige des users non-admin portant des permissions `commercial.clients.*` qui * ne sont declarees qu'en ERP-59 (tests RBAC complets en ERP-60). * diff --git a/tests/Module/Commercial/Api/ClientRBACMatrixTest.php b/tests/Module/Commercial/Api/ClientRBACMatrixTest.php index 014ab66..bd51856 100644 --- a/tests/Module/Commercial/Api/ClientRBACMatrixTest.php +++ b/tests/Module/Commercial/Api/ClientRBACMatrixTest.php @@ -14,8 +14,7 @@ use Symfony\Component\Console\Output\NullOutput; /** * Matrice RBAC complete du repertoire clients par role metier (spec-back M1 * § 2.7 + cahier ERP-74). Valide 200/403 par verbe et par onglet pour - * bureau / compta / commerciale / usine, plus le durcissement RG-1.04 - * (Commerciale) au POST. + * bureau / compta / commerciale / usine. * * Les comptes demo et la matrice sont seedes via la commande reelle * `app:seed-rbac --with-demo-users` (le MEME chemin qu'en recette), idempotente. @@ -174,14 +173,14 @@ final class ClientRBACMatrixTest extends AbstractCommercialApiTestCase $client->request('GET', '/api/clients', ['headers' => ['Accept' => self::LD]]); self::assertResponseStatusCodeSame(200); - // manage : la creation passe la security d'operation (pas un 403 comme - // Compta) mais bute sur RG-1.04 (onglet Information incomplet) -> 422. - // C'est la preuve que Commerciale porte `manage` (sinon 403). + // manage : la creation passe la security d'operation et aboutit -> 201 + // (l'onglet Information est facultatif pour tous depuis le retrait de + // RG-1.04). C'est la preuve que Commerciale porte `manage` (sinon 403). $client->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => $this->validMainPayload('Commerciale Post'), ]); - self::assertResponseStatusCodeSame(422); + self::assertResponseStatusCodeSame(201); // PAS accounting : edition onglet Comptabilite refusee $client->request('PATCH', '/api/clients/'.$seed->getId(), [ @@ -198,27 +197,6 @@ final class ClientRBACMatrixTest extends AbstractCommercialApiTestCase self::assertResponseStatusCodeSame(403); } - public function testRG104CommercialePostIncompleteIs422AdminIs201(): void - { - $cat = $this->createCategory('SECTEUR'); - - // RG-1.04 durcie : Commerciale POST sans onglet Information complet -> 422. - $commerciale = $this->authAs('commerciale'); - $commerciale->request('POST', '/api/clients', [ - 'headers' => ['Content-Type' => self::LD], - 'json' => $this->validMainPayload('RG104 Commerciale', $cat->getId()), - ]); - self::assertResponseStatusCodeSame(422); - - // Meme payload par un Admin (non gate par RG-1.04) -> 201. - $admin = $this->createAdminClient(); - $admin->request('POST', '/api/clients', [ - 'headers' => ['Content-Type' => self::LD], - 'json' => $this->validMainPayload('RG104 Admin', $cat->getId()), - ]); - self::assertResponseStatusCodeSame(201); - } - public function testComptaFullRepresentationPatchWithUnchangedCategoriesIsNotForbidden(): void { // FIX review MR #40 : un Compta (accounting.manage, PAS manage) faisant un diff --git a/tests/Module/Commercial/Api/ClientSecurityTest.php b/tests/Module/Commercial/Api/ClientSecurityTest.php index 4d9abf1..f967ed9 100644 --- a/tests/Module/Commercial/Api/ClientSecurityTest.php +++ b/tests/Module/Commercial/Api/ClientSecurityTest.php @@ -12,8 +12,8 @@ namespace App\Tests\Module\Commercial\Api; * - 403 si l'utilisateur authentifie ne porte pas `commercial.clients.view`. * * ⚠ La matrice RBAC differenciee par role metier (bureau / compta / commerciale - * / usine) et le test fonctionnel RG-1.04 sont DELEGUES a ERP-74 (#493) : ils - * exigent les roles seedes apres le merge de la stack. NE PAS les ajouter ici. + * / usine) est DELEGUEE a ERP-74 (#493) : elle exige les roles seedes apres le + * merge de la stack. NE PAS l'ajouter ici. * * @internal */ diff --git a/tests/Module/Commercial/Unit/ClientProcessorTest.php b/tests/Module/Commercial/Unit/ClientProcessorTest.php index 1ac8905..4fce0cf 100644 --- a/tests/Module/Commercial/Unit/ClientProcessorTest.php +++ b/tests/Module/Commercial/Unit/ClientProcessorTest.php @@ -9,7 +9,6 @@ use ApiPlatform\State\ProcessorInterface; use ApiPlatform\Validator\Exception\ValidationException; use App\Module\Commercial\Application\Service\ClientFieldNormalizer; use App\Module\Commercial\Application\Validator\ClientAccountingCompletenessValidator; -use App\Module\Commercial\Application\Validator\ClientInformationCompletenessValidator; use App\Module\Commercial\Domain\Entity\Bank; use App\Module\Commercial\Domain\Entity\Client; use App\Module\Commercial\Domain\Entity\ClientRib; @@ -17,8 +16,6 @@ use App\Module\Commercial\Domain\Entity\PaymentDelay; use App\Module\Commercial\Domain\Entity\PaymentType; use App\Module\Commercial\Domain\Entity\TvaMode; use App\Module\Commercial\Infrastructure\ApiPlatform\State\Processor\ClientProcessor; -use App\Shared\Domain\Contract\BusinessRoleAwareInterface; -use App\Shared\Domain\Security\BusinessRoles; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\UnitOfWork; use PHPUnit\Framework\TestCase; @@ -27,13 +24,11 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; -use Symfony\Component\Security\Core\User\UserInterface; /** * Tests unitaires du ClientProcessor : gating par permission (accounting.manage * / archive / RG-1.28 strict) et regles metier non testables en HTTP admin - * (RG-1.04 Commerciale, RG-1.12 Virement, RG-1.13 LCR), grace a un Security et - * un RequestStack stubbes. + * (RG-1.12 Virement, RG-1.13 LCR), grace a un Security et un RequestStack stubbes. * * @internal */ @@ -342,62 +337,6 @@ final class ClientProcessorTest extends TestCase self::assertInstanceOf(Client::class, $processor->process($client, $this->operation())); } - public function testCommercialeIncompleteInformationIsUnprocessable(): void - { - // RG-1.04 : role Commerciale + onglet Information incomplet -> 422. - $client = $this->minimalClient(); - $client->setDescription('Une description'); // les autres champs Information restent null - - $processor = $this->makeProcessor( - granted: [], - payload: ['description' => 'Une description'], - user: $this->commercialeUser(), - ); - - $this->expectException(ValidationException::class); - $processor->process($client, $this->operation()); - } - - public function testCommercialeIncompleteInformationOnNonInformationPatchIsUnprocessable(): void - { - // RG-1.04 durcie (ERP-74) : pour une Commerciale, la completude de - // l'onglet Information est exigee meme quand le payload ne touche PAS - // l'onglet Information (ici seulement companyName). L'ancienne condition - // d'intersection avec INFORMATION_FIELDS a ete retiree. - $client = $this->minimalClient(); - $client->setCompanyName('Renamed Co'); // onglet principal uniquement, Information vide - - $processor = $this->makeProcessor( - granted: ['commercial.clients.manage'], - payload: ['companyName' => 'Renamed Co'], - user: $this->commercialeUser(), - managed: true, - originalData: [ - 'companyName' => 'TEST CO', - 'triageService' => false, - 'isArchived' => false, - ], - ); - - $this->expectException(ValidationException::class); - $processor->process($client, $this->operation()); - } - - public function testNonCommercialeSkipsInformationCompleteness(): void - { - // Meme payload incomplet, mais user non-Commerciale -> aucun blocage. - $client = $this->minimalClient(); - $client->setDescription('Une description'); - - $processor = $this->makeProcessor( - granted: [], - payload: ['description' => 'Une description'], - user: null, - ); - - self::assertInstanceOf(Client::class, $processor->process($client, $this->operation())); - } - /** * @param list $granted Permissions accordees a l'utilisateur courant * @param array $payload Corps JSON simule de la requete @@ -407,7 +346,6 @@ final class ClientProcessorTest extends TestCase private function makeProcessor( array $granted, array $payload, - ?UserInterface $user = null, bool $managed = false, array $originalData = [], ): ClientProcessor { @@ -422,7 +360,6 @@ final class ClientProcessorTest extends TestCase $security->method('isGranted')->willReturnCallback( static fn (mixed $attribute): bool => is_string($attribute) && in_array($attribute, $granted, true), ); - $security->method('getUser')->willReturn($user); $requestStack = new RequestStack(); $requestStack->push(new Request([], [], [], [], [], [], json_encode($payload, JSON_THROW_ON_ERROR))); @@ -440,7 +377,6 @@ final class ClientProcessorTest extends TestCase return new ClientProcessor( $persist, new ClientFieldNormalizer(), - new ClientInformationCompletenessValidator(), new ClientAccountingCompletenessValidator(), $security, $requestStack, @@ -493,26 +429,4 @@ final class ClientProcessorTest extends TestCase { return $this->createStub(Operation::class); } - - private function commercialeUser(): UserInterface - { - return new class implements UserInterface, BusinessRoleAwareInterface { - public function hasBusinessRole(string $roleCode): bool - { - return BusinessRoles::COMMERCIALE === $roleCode; - } - - public function getRoles(): array - { - return ['ROLE_USER']; - } - - public function eraseCredentials(): void {} - - public function getUserIdentifier(): string - { - return 'commerciale-test'; - } - }; - } }