Files
Starseed/tests/Module/Commercial/Unit/ClientReadGroupContextBuilderTest.php
T
Matthieu e1b8f8a28d feat(commercial) : add Client API Platform provider + processor + business rules
Branche l'API REST du repertoire clients (M1) sur l'entite Client preparee en
ERP-54. Operations GetCollection / Get / Post / Patch (pas de Delete au M1 :
l'archivage passe par PATCH isArchived).

ClientProvider :
- liste paginee (Paginator ORM, aligne sur la convention ERP-72) + echappatoire
  ?pagination=false
- exclut archives + soft-deletes par defaut (RG-1.24), ?includeArchived=true
  reintegre les archives (RG-1.25)
- tri companyName ASC (RG-1.26), filtres ?search (fuzzy companyName/lastName/
  email) et ?categoryType=<code>
- detail : 404 sur soft-delete, embarque contacts/adresses/ribs

ClientProcessor :
- normalisation serveur via ClientFieldNormalizer (RG-1.18 a 1.21)
- 409 sur doublon de nom de societe (RG-1.16) ; 409 dedie sur conflit de
  restauration (RG-1.23)
- gating par onglet : champ comptable -> accounting.manage, isArchived ->
  archive, mode strict 403 sur tout le payload (RG-1.28) ; archivage exclusif
  (RG-1.22) + pose/retrait archivedAt
- regles metier RG-1.01 (prenom/nom), RG-1.03 (distributor/broker exclusifs +
  controle du type de categorie), RG-1.12 (Virement -> banque), RG-1.13 (LCR ->
  >= 1 RIB), RG-1.04 (completude Information pour le role Commerciale)

Lecture comptable conditionnelle : ClientReadGroupContextBuilder ajoute le
groupe client:read:accounting selon commercial.clients.accounting.view.

Resolution des references categorie : CategoryReferenceDenormalizer resout les
IRI vers Category quand la propriete est type-hintee par le contrat
CategoryInterface (denormalisation impossible sur une interface sinon).

Contrats Shared :
- CategoryInterface::getCategoryTypeCode() (implemente par Category) pour la
  verification de type sans import inter-modules
- BusinessRoleAwareInterface (implemente par User) + BusinessRoles::COMMERCIALE
  pour detecter le role metier ; le code de role sera seede par ERP-74 et
  reutilise par ERP-59/60. RG-1.04 reste dormante tant qu'aucun user ne porte
  ce role.

Coordination stack :
- chaines de permission commercial.clients.* referencees ici, declarees en
  ERP-59 (tests RBAC complets en ERP-60)
- config globale de pagination (itemsPerPage client, max 50) portee par ERP-72
- referentiels comptables (PaymentType/Bank/...) exposes en ERP-56

Tests : 31 tests Commercial (integration admin sur les regles metier + unitaires
sur le gating, RG-1.04/1.12/1.13 et le context builder). Suite complete verte
(339 tests).
2026-05-29 16:41:40 +02:00

86 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Module\Commercial\Unit;
use ApiPlatform\State\SerializerContextBuilderInterface;
use App\Module\Commercial\Domain\Entity\Client;
use App\Module\Commercial\Infrastructure\ApiPlatform\Serializer\ClientReadGroupContextBuilder;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests unitaires du context builder qui ajoute conditionnellement le groupe
* de lecture `client:read:accounting` selon la permission accounting.view
* (§ 2.7 / § 4.1 / § 4.2).
*
* @internal
*/
final class ClientReadGroupContextBuilderTest extends TestCase
{
public function testAddsAccountingGroupForClientReadWhenGranted(): void
{
$builder = $this->builder(
baseContext: ['resource_class' => Client::class, 'groups' => ['client:read', 'default:read']],
granted: true,
);
$context = $builder->createFromRequest(new Request(), true);
self::assertContains('client:read:accounting', $context['groups']);
}
public function testDoesNotAddAccountingGroupWhenNotGranted(): void
{
$builder = $this->builder(
baseContext: ['resource_class' => Client::class, 'groups' => ['client:read', 'default:read']],
granted: false,
);
$context = $builder->createFromRequest(new Request(), true);
self::assertNotContains('client:read:accounting', $context['groups']);
}
public function testDoesNotAddAccountingGroupOnWrite(): void
{
$builder = $this->builder(
baseContext: ['resource_class' => Client::class, 'groups' => ['client:write:main']],
granted: true,
);
// normalization = false -> ecriture : pas de groupe de lecture ajoute.
$context = $builder->createFromRequest(new Request(), false);
self::assertNotContains('client:read:accounting', $context['groups']);
}
public function testIgnoresOtherResources(): void
{
$builder = $this->builder(
baseContext: ['resource_class' => 'App\Other\Resource', 'groups' => ['other:read']],
granted: true,
);
$context = $builder->createFromRequest(new Request(), true);
self::assertSame(['other:read'], $context['groups']);
}
/**
* @param array<string, mixed> $baseContext
*/
private function builder(array $baseContext, bool $granted): ClientReadGroupContextBuilder
{
$decorated = $this->createStub(SerializerContextBuilderInterface::class);
$decorated->method('createFromRequest')->willReturn($baseContext);
$security = $this->createStub(Security::class);
$security->method('isGranted')->willReturn($granted);
return new ClientReadGroupContextBuilder($decorated, $security);
}
}