Add SIRET/RNA verification, organizer management, registration flow pages

SIRET/RNA verification:
- Create SiretService with API gouv lookup + JOAFE RNA lookup + cache pool (24h)
- Verification page: declared info vs API data side by side
- Display NAF code + label (from naf.json), nature juridique code + label
- Association/Entreprise/EI badges, ESS badge, RNA, coordonnees lat/long
- JOAFE section: objet, regime, domaine, dates, lieu, PDF download link
- Tranche effectif with readable labels
- Refresh cache button
- Page restricted to non-approved organizers only

Organizer approval flow:
- Approval form with offer (free/basic/custom) and commission rate (default 3%)
- Add commissionRate field to User entity + migration
- Rejection form with required reason textarea, sent in email
- Edit page for approved organizers: all fields modifiable
- Modify button in approved organizers table

Registration flow pages:
- Post-registration success page with email verification message
- Organizer gets additional 48h staff review notice
- Post-email-verification page: confirmed for buyers, 48h notice for organizers

Dashboard:
- Simplified Meilisearch sync to single button

Tests: SiretServiceTest (9), AdminControllerTest (31), RegistrationControllerTest updated, UserTest updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-19 20:25:04 +01:00
parent b1912f4362
commit 100ff96c70
18 changed files with 7858 additions and 56 deletions

View File

@@ -404,6 +404,122 @@ class AdminControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
}
public function testSiretCheckPage(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$orga = $this->createOrganizer($em);
$client->loginUser($admin);
$client->request('GET', '/admin/organisateur/'.$orga->getId().'/siret');
self::assertResponseIsSuccessful();
}
public function testSiretCheckRedirectsIfApproved(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$orga = $this->createOrganizer($em);
$orga->setIsApproved(true);
$em->flush();
$client->loginUser($admin);
$client->request('GET', '/admin/organisateur/'.$orga->getId().'/siret');
self::assertResponseRedirects('/admin/organisateurs');
}
public function testSiretCheckWithoutSiret(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$orga = new User();
$orga->setEmail('test-no-siret-'.uniqid().'@example.com');
$orga->setFirstName('No');
$orga->setLastName('Siret');
$orga->setPassword('$2y$13$hashed');
$orga->setRoles(['ROLE_ORGANIZER']);
$em->persist($orga);
$em->flush();
$client->loginUser($admin);
$client->request('GET', '/admin/organisateur/'.$orga->getId().'/siret');
self::assertResponseRedirects('/admin/organisateurs');
}
public function testSiretRefresh(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$orga = $this->createOrganizer($em);
$client->loginUser($admin);
$client->request('POST', '/admin/organisateur/'.$orga->getId().'/siret/refresh');
self::assertResponseRedirects('/admin/organisateur/'.$orga->getId().'/siret');
}
public function testEditOrganizerPage(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$orga = $this->createOrganizer($em);
$orga->setIsApproved(true);
$orga->setOffer('free');
$orga->setCommissionRate(3.0);
$em->flush();
$client->loginUser($admin);
$client->request('GET', '/admin/organisateur/'.$orga->getId().'/modifier');
self::assertResponseIsSuccessful();
}
public function testEditOrganizerSubmit(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$orga = $this->createOrganizer($em);
$orga->setIsApproved(true);
$orga->setOffer('free');
$orga->setCommissionRate(3.0);
$em->flush();
$client->loginUser($admin);
$client->request('POST', '/admin/organisateur/'.$orga->getId().'/modifier', [
'offer' => 'custom',
'commission_rate' => '0.5',
]);
self::assertResponseRedirects('/admin/organisateurs?tab=approved');
$em->refresh($orga);
self::assertSame('custom', $orga->getOffer());
self::assertSame(0.5, $orga->getCommissionRate());
}
public function testEditOrganizerRedirectsIfNotApproved(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$orga = $this->createOrganizer($em);
$client->loginUser($admin);
$client->request('GET', '/admin/organisateur/'.$orga->getId().'/modifier');
self::assertResponseRedirects('/admin/organisateurs');
}
public function testApproveOrganizer(): void
{
$client = static::createClient();
@@ -422,12 +538,17 @@ class AdminControllerTest extends WebTestCase
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$client->loginUser($admin);
$client->request('POST', '/admin/organisateur/'.$orga->getId().'/approuver');
$client->request('POST', '/admin/organisateur/'.$orga->getId().'/approuver', [
'offer' => 'basic',
'commission_rate' => '1.5',
]);
self::assertResponseRedirects('/admin/organisateurs');
$em->refresh($orga);
self::assertTrue($orga->isApproved());
self::assertSame('basic', $orga->getOffer());
self::assertSame(1.5, $orga->getCommissionRate());
}
public function testRejectOrganizer(): void
@@ -444,7 +565,9 @@ class AdminControllerTest extends WebTestCase
static::getContainer()->set(MailerService::class, $mailer);
$client->loginUser($admin);
$client->request('POST', '/admin/organisateur/'.$orgaId.'/refuser');
$client->request('POST', '/admin/organisateur/'.$orgaId.'/refuser', [
'reason' => 'SIRET invalide, activite non conforme.',
]);
self::assertResponseRedirects('/admin/organisateurs');

View File

@@ -4,6 +4,7 @@ namespace App\Tests\Controller;
use App\Entity\User;
use App\Service\MailerService;
use App\Service\MeilisearchService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
@@ -51,7 +52,8 @@ class RegistrationControllerTest extends WebTestCase
'password' => 'Password123!',
]);
self::assertResponseRedirects('/connexion');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', 'Compte cree');
}
public function testRegistrationAsOrganizer(): void
@@ -76,7 +78,9 @@ class RegistrationControllerTest extends WebTestCase
'phone' => '0612345678',
]);
self::assertResponseRedirects('/connexion');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', 'Compte cree');
self::assertSelectorTextContains('body', '48h');
}
public function testRegistrationWithDuplicateEmail(): void
@@ -110,6 +114,9 @@ class RegistrationControllerTest extends WebTestCase
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$meilisearch = $this->createMock(MeilisearchService::class);
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$user = new User();
$user->setEmail('test-verify-'.uniqid().'@example.com');
$user->setFirstName('Test');
@@ -122,7 +129,8 @@ class RegistrationControllerTest extends WebTestCase
$token = $user->getEmailVerificationToken();
$client->request('GET', '/verification-email/'.$token);
self::assertResponseRedirects('/connexion');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('h1', 'Email verifie');
$em->refresh($user);
self::assertTrue($user->isVerified());
@@ -139,6 +147,9 @@ class RegistrationControllerTest extends WebTestCase
$mailer->expects(self::exactly(2))->method('sendEmail');
static::getContainer()->set(MailerService::class, $mailer);
$meilisearch = $this->createMock(MeilisearchService::class);
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$user = new User();
$user->setEmail('test-orga-verify-'.uniqid().'@example.com');
$user->setFirstName('Marie');
@@ -158,7 +169,8 @@ class RegistrationControllerTest extends WebTestCase
$token = $user->getEmailVerificationToken();
$client->request('GET', '/verification-email/'.$token);
self::assertResponseRedirects('/connexion');
self::assertResponseIsSuccessful();
self::assertSelectorTextContains('body', '48h');
}
public function testVerifyEmailWithInvalidToken(): void

View File

@@ -136,12 +136,14 @@ class UserTest extends TestCase
self::assertFalse($user->isApproved());
self::assertNull($user->getOffer());
self::assertNull($user->getCommissionRate());
$result = $user->setIsApproved(true)->setOffer('custom');
$result = $user->setIsApproved(true)->setOffer('custom')->setCommissionRate(1.5);
self::assertSame($user, $result);
self::assertTrue($user->isApproved());
self::assertSame('custom', $user->getOffer());
self::assertSame(1.5, $user->getCommissionRate());
}
public function testEmailVerificationFields(): void

View File

@@ -0,0 +1,153 @@
<?php
namespace App\Tests\Service;
use App\Service\SiretService;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
class SiretServiceTest extends TestCase
{
public function testLookupReturnsDataWithLabels(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('toArray')->willReturn([
'results' => [[
'nom_complet' => 'E-COSPLAY',
'nature_juridique' => '9220',
'activite_principale' => '93.29Z',
'siren' => '943121517',
]],
]);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')->willReturn($response);
$cache = $this->createMock(CacheInterface::class);
$cache->method('get')->willReturnCallback(fn (string $key, callable $callback) => $callback($this->createMock(ItemInterface::class)));
$projectDir = \dirname(__DIR__, 2);
$service = new SiretService($httpClient, $cache, $projectDir);
$data = $service->lookup('94312151700016');
self::assertNotNull($data);
self::assertSame('E-COSPLAY', $data['nom_complet']);
self::assertSame('Association declaree', $data['libelle_nature_juridique']);
self::assertNotNull($data['libelle_activite_principale']);
}
public function testLookupReturnsNullWhenNotFound(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('toArray')->willReturn(['results' => []]);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')->willReturn($response);
$cache = $this->createMock(CacheInterface::class);
$cache->method('get')->willReturnCallback(fn (string $key, callable $callback) => $callback($this->createMock(ItemInterface::class)));
$service = new SiretService($httpClient, $cache, \dirname(__DIR__, 2));
self::assertNull($service->lookup('00000000000000'));
}
public function testLookupReturnsNullOnApiError(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')->willThrowException(new \RuntimeException('API down'));
$cache = $this->createMock(CacheInterface::class);
$cache->method('get')->willReturnCallback(fn (string $key, callable $callback) => $callback($this->createMock(ItemInterface::class)));
$service = new SiretService($httpClient, $cache, \dirname(__DIR__, 2));
self::assertNull($service->lookup('00000000000000'));
}
public function testGetNatureJuridiqueLabel(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$cache = $this->createMock(CacheInterface::class);
$service = new SiretService($httpClient, $cache, \dirname(__DIR__, 2));
self::assertSame('Association declaree', $service->getNatureJuridiqueLabel('9220'));
self::assertSame('SAS', $service->getNatureJuridiqueLabel('5710'));
self::assertNull($service->getNatureJuridiqueLabel('0000'));
}
public function testGetNafLabel(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$cache = $this->createMock(CacheInterface::class);
$service = new SiretService($httpClient, $cache, \dirname(__DIR__, 2));
self::assertNotNull($service->getNafLabel('93.29Z'));
self::assertNull($service->getNafLabel('XX.XXX'));
}
public function testLookupRnaReturnsData(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('toArray')->willReturn([
'records' => [[
'fields' => [
'numero_rna' => 'W022006988',
'objet' => 'promotion du cosplay',
'association_type_libelle' => 'Associations loi du 1er juillet 1901',
],
]],
]);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')->willReturn($response);
$cache = $this->createMock(CacheInterface::class);
$cache->method('get')->willReturnCallback(fn (string $key, callable $callback) => $callback($this->createMock(ItemInterface::class)));
$service = new SiretService($httpClient, $cache, \dirname(__DIR__, 2));
$data = $service->lookupRna('W022006988');
self::assertNotNull($data);
self::assertSame('W022006988', $data['numero_rna']);
self::assertSame('promotion du cosplay', $data['objet']);
}
public function testLookupRnaReturnsNullWhenNotFound(): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('toArray')->willReturn(['records' => []]);
$httpClient = $this->createMock(HttpClientInterface::class);
$httpClient->method('request')->willReturn($response);
$cache = $this->createMock(CacheInterface::class);
$cache->method('get')->willReturnCallback(fn (string $key, callable $callback) => $callback($this->createMock(ItemInterface::class)));
$service = new SiretService($httpClient, $cache, \dirname(__DIR__, 2));
self::assertNull($service->lookupRna('W000000000'));
}
public function testClearCacheWithRna(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$cache = $this->createMock(CacheInterface::class);
$cache->expects(self::exactly(2))->method('delete');
$service = new SiretService($httpClient, $cache, \dirname(__DIR__, 2));
$service->clearCache('12345678901234', 'W022006988');
}
public function testGetNafLabelMissingFile(): void
{
$httpClient = $this->createMock(HttpClientInterface::class);
$cache = $this->createMock(CacheInterface::class);
$service = new SiretService($httpClient, $cache, '/nonexistent');
self::assertNull($service->getNafLabel('93.29Z'));
}
}