fix(commercial) : retrait RG-1.04 (onglet Information facultatif pour tous)

L'onglet Information n'est plus obligatoire pour le role metier Commerciale : il devient facultatif pour tous les roles, cote back comme cote front (le front l'etait deja).

- Suppression du validateur ClientInformationCompletenessValidator et de son gating (validateInformationCompleteness / currentUserIsCommerciale) dans ClientProcessor. Security conserve (gating accounting/archive/manage).

- Tests : retrait des 3 tests RG-1.04 (ClientProcessorTest) ; POST Commerciale attendu en 201 et suppression du test dedie (ClientRBACMatrixTest).

- Coherence : commentaires de colonnes BDD (catalogue + migration d'init) passes a « Facultatif », nettoyage des references RG-1.04 (BusinessRoles, RbacSeeder, User, fixtures, front, specs M1, README). Le role metier Commerciale et ses permissions RBAC restent inchanges.

- Pas de migration de schema (colonnes Information deja nullable).
This commit is contained in:
2026-06-08 12:17:16 +02:00
parent 613aaa88c9
commit 27f2dcd4c0
20 changed files with 70 additions and 287 deletions
@@ -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).
*
@@ -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
@@ -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
*/
@@ -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<string> $granted Permissions accordees a l'utilisateur courant
* @param array<string, mixed> $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';
}
};
}
}