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

@@ -1,19 +1,6 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null
pools:
siret.cache:
adapter: cache.app
default_lifetime: 86400

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260319191415 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" ADD commission_rate DOUBLE PRECISION DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE "user" DROP commission_rate');
}
}

6830
naf.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ namespace App\Controller;
use App\Entity\User;
use App\Service\MailerService;
use App\Service\MeilisearchService;
use App\Service\SiretService;
use Doctrine\ORM\EntityManagerInterface;
use Knp\Component\Pager\PaginatorInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -290,9 +291,14 @@ class AdminController extends AbstractController
}
#[Route('/organisateur/{id}/approuver', name: 'app_admin_approve_organizer', methods: ['POST'])]
public function approveOrganizer(User $user, EntityManagerInterface $em, MailerService $mailerService, MeilisearchService $meilisearch): Response
public function approveOrganizer(User $user, Request $request, EntityManagerInterface $em, MailerService $mailerService, MeilisearchService $meilisearch): Response
{
$offer = $request->request->getString('offer', 'free');
$commissionRate = (float) $request->request->getString('commission_rate', '3');
$user->setIsApproved(true);
$user->setOffer($offer);
$user->setCommissionRate($commissionRate);
$em->flush();
$meilisearch->createIndexIfNotExists('organizers');
@@ -324,22 +330,101 @@ class AdminController extends AbstractController
}
#[Route('/organisateur/{id}/refuser', name: 'app_admin_reject_organizer', methods: ['POST'])]
public function rejectOrganizer(User $user, EntityManagerInterface $em, MailerService $mailerService): Response
public function rejectOrganizer(User $user, Request $request, EntityManagerInterface $em, MailerService $mailerService): Response
{
$reason = trim($request->request->getString('reason'));
$email = $user->getEmail();
$firstName = $user->getFirstName();
$lastName = $user->getLastName();
$em->remove($user);
$em->flush();
$mailerService->sendEmail(
to: $user->getEmail(),
to: $email,
subject: 'Votre demande de compte organisateur a ete refusee - E-Ticket',
content: $this->renderView('email/organizer_rejected.html.twig', [
'firstName' => $user->getFirstName(),
'firstName' => $firstName,
'reason' => $reason,
]),
withUnsubscribe: false,
);
$this->addFlash('success', sprintf('Demande de %s %s refusee.', $user->getFirstName(), $user->getLastName()));
$this->addFlash('success', sprintf('Demande de %s %s refusee.', $firstName, $lastName));
return $this->redirectToRoute('app_admin_organizers');
}
#[Route('/organisateur/{id}/siret', name: 'app_admin_siret_check')]
public function siretCheck(User $user, SiretService $siretService): Response
{
if ($user->isApproved()) {
return $this->redirectToRoute('app_admin_organizers');
}
$siret = $user->getSiret();
if (!$siret) {
$this->addFlash('error', 'Aucun SIRET renseigne.');
return $this->redirectToRoute('app_admin_organizers');
}
$data = $siretService->lookup($siret);
$rna = $data['complements']['identifiant_association'] ?? null;
$rnaData = $rna ? $siretService->lookupRna($rna) : null;
return $this->render('admin/siret_check.html.twig', [
'user' => $user,
'siret' => $siret,
'data' => $data,
'rnaData' => $rnaData,
]);
}
#[Route('/organisateur/{id}/siret/refresh', name: 'app_admin_siret_refresh', methods: ['POST'])]
public function siretRefresh(User $user, SiretService $siretService): Response
{
$siret = $user->getSiret();
if ($siret) {
$data = $siretService->lookup($siret);
$rna = $data['complements']['identifiant_association'] ?? null;
$siretService->clearCache($siret, $rna);
$this->addFlash('success', 'Cache SIRET vide. Les donnees ont ete rechargees.');
}
return $this->redirectToRoute('app_admin_siret_check', ['id' => $user->getId()]);
}
#[Route('/organisateur/{id}/modifier', name: 'app_admin_edit_organizer', methods: ['GET', 'POST'])]
public function editOrganizer(User $user, Request $request, EntityManagerInterface $em): Response
{
if (!$user->isApproved()) {
return $this->redirectToRoute('app_admin_organizers');
}
if ($request->isMethod('POST')) {
$user->setFirstName(trim($request->request->getString('first_name')));
$user->setLastName(trim($request->request->getString('last_name')));
$user->setEmail(trim($request->request->getString('email')));
$user->setPhone(trim($request->request->getString('phone')));
$user->setCompanyName(trim($request->request->getString('company_name')));
$user->setSiret(trim($request->request->getString('siret')));
$user->setAddress(trim($request->request->getString('address')));
$user->setPostalCode(trim($request->request->getString('postal_code')));
$user->setCity(trim($request->request->getString('city')));
$user->setOffer($request->request->getString('offer'));
$user->setCommissionRate((float) $request->request->getString('commission_rate'));
$em->flush();
$this->addFlash('success', sprintf('Organisateur %s %s mis a jour.', $user->getFirstName(), $user->getLastName()));
return $this->redirectToRoute('app_admin_organizers', ['tab' => 'approved']);
}
return $this->render('admin/edit_organizer.html.twig', [
'user' => $user,
]);
}
}

View File

@@ -72,9 +72,9 @@ class RegistrationController extends AbstractController
withUnsubscribe: false,
);
$this->addFlash('success', 'Compte cree ! Un email de verification vous a ete envoye.');
return $this->redirectToRoute('app_login');
return $this->render('security/register_success.html.twig', [
'isOrganizer' => 'organizer' === $type,
]);
}
foreach ($errors as $error) {
@@ -145,8 +145,8 @@ class RegistrationController extends AbstractController
);
}
$this->addFlash('success', 'Votre adresse email a ete verifiee. Vous pouvez vous connecter.');
return $this->redirectToRoute('app_login');
return $this->render('security/email_verified.html.twig', [
'isOrganizer' => \in_array('ROLE_ORGANIZER', $user->getRoles(), true),
]);
}
}

View File

@@ -82,6 +82,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(length: 255, nullable: true)]
private ?string $offer = null;
#[ORM\Column(nullable: true)]
private ?float $commissionRate = null;
#[ORM\Column(length: 64, nullable: true)]
private ?string $emailVerificationToken = null;
@@ -281,6 +284,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this->updatedAt;
}
public function getCommissionRate(): ?float
{
return $this->commissionRate;
}
public function setCommissionRate(?float $commissionRate): static
{
$this->commissionRate = $commissionRate;
return $this;
}
public function getResetCode(): ?string
{
return $this->resetCode;

View File

@@ -0,0 +1,142 @@
<?php
namespace App\Service;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* @phpstan-type NafEntry array{id: string, label: string}
*/
class SiretService
{
private const API_URL = 'https://recherche-entreprises.api.gouv.fr/search';
private const NATURE_JURIDIQUE = [
'1000' => 'Entrepreneur individuel',
'5410' => 'SARL',
'5499' => 'SARL unipersonnelle',
'5505' => 'SA a conseil d\'administration',
'5510' => 'SA a directoire',
'5599' => 'SA a conseil d\'administration',
'5710' => 'SAS',
'5720' => 'SASU',
'6540' => 'Societe cooperative',
'9210' => 'Association non declaree',
'9220' => 'Association declaree',
'9221' => 'Association declaree d\'insertion',
'9222' => 'Association intermediaire',
'9223' => 'Association reconnue d\'utilite publique',
'9224' => 'Association de droit local',
'9230' => 'Association en justice',
'9240' => 'Association agree d\'education populaire',
'9260' => 'Association sportive agree',
'9300' => 'Fondation',
];
/** @var array<string, string>|null */
private ?array $nafCodes = null;
public function __construct(
private HttpClientInterface $httpClient,
#[Autowire(service: 'siret.cache')] private CacheInterface $cache,
#[Autowire('%kernel.project_dir%')] private string $projectDir,
) {
}
/**
* @return array<string, mixed>|null
*/
public function lookup(string $siret): ?array
{
return $this->cache->get('siret_'.$siret, function (ItemInterface $item) use ($siret): ?array {
$item->expiresAfter(86400);
try {
$response = $this->httpClient->request('GET', self::API_URL, [
'query' => ['q' => $siret, 'per_page' => 1],
]);
$result = $response->toArray(false);
if (isset($result['results'][0])) {
$data = $result['results'][0];
$data['libelle_nature_juridique'] = self::NATURE_JURIDIQUE[$data['nature_juridique'] ?? ''] ?? null;
$data['libelle_activite_principale'] = $this->getNafLabel($data['activite_principale'] ?? '');
return $data;
}
} catch (\Throwable) {
// API indisponible
}
return null;
});
}
/**
* @return array<string, mixed>|null
*/
public function lookupRna(string $rna): ?array
{
return $this->cache->get('rna_'.$rna, function (ItemInterface $item) use ($rna): ?array {
$item->expiresAfter(86400);
try {
$response = $this->httpClient->request('GET', 'https://journal-officiel-datadila.opendatasoft.com/api/records/1.0/search/', [
'query' => ['dataset' => 'jo_associations', 'q' => $rna, 'rows' => 1],
]);
$result = $response->toArray(false);
if (isset($result['records'][0]['fields'])) {
return $result['records'][0]['fields'];
}
} catch (\Throwable) {
// API indisponible
}
return null;
});
}
public function clearCache(string $siret, ?string $rna = null): void
{
$this->cache->delete('siret_'.$siret);
if ($rna) {
$this->cache->delete('rna_'.$rna);
}
}
public function getNatureJuridiqueLabel(string $code): ?string
{
return self::NATURE_JURIDIQUE[$code] ?? null;
}
public function getNafLabel(string $code): ?string
{
if (null === $this->nafCodes) {
$path = $this->projectDir.'/naf.json';
if (!file_exists($path)) {
return null;
}
$json = file_get_contents($path);
if (false === $json) {
return null;
}
/** @var list<NafEntry> $entries */
$entries = json_decode($json, true) ?? [];
$this->nafCodes = [];
foreach ($entries as $entry) {
$this->nafCodes[$entry['id']] = $entry['label'];
}
}
return $this->nafCodes[$code] ?? null;
}
}

View File

@@ -19,11 +19,7 @@
</div>
</div>
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Synchronisation Meilisearch</h2>
<p class="text-sm text-gray-500 font-bold" style="margin-bottom:1rem;">Synchronise manuellement les acheteurs verifies dans l'index Meilisearch.</p>
<form method="post" action="{{ path('app_admin_sync_meilisearch') }}">
<button type="submit" style="padding:0.5rem 1rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;cursor:pointer;" class="font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Synchroniser les acheteurs</button>
</form>
</div>
<form method="post" action="{{ path('app_admin_sync_meilisearch') }}" style="display:inline;">
<button type="submit" style="padding:0.5rem 1rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;cursor:pointer;" class="font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Sync Meilisearch</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,115 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Modifier {{ user.companyName }}{% endblock %}
{% block body %}
<div style="margin-bottom:2rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:0.5rem;">Modifier l'organisateur</h1>
<p class="font-bold text-gray-500 italic">{{ user.companyName }}{{ user.email }}</p>
</div>
<form method="post" action="{{ path('app_admin_edit_organizer', {id: user.id}) }}">
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;margin-bottom:2rem;">
<div style="flex:1;min-width:300px;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Informations personnelles</h2>
<div style="display:flex;flex-direction:column;gap:1rem;">
<div style="display:flex;gap:1rem;">
<div style="flex:1;">
<label for="edit_last_name" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Nom</label>
<input type="text" id="edit_last_name" name="last_name" value="{{ user.lastName }}" required
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
<div style="flex:1;">
<label for="edit_first_name" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Prenom</label>
<input type="text" id="edit_first_name" name="first_name" value="{{ user.firstName }}" required
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
</div>
<div>
<label for="edit_email" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Email</label>
<input type="email" id="edit_email" name="email" value="{{ user.email }}" required
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
<div>
<label for="edit_phone" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Telephone</label>
<input type="tel" id="edit_phone" name="phone" value="{{ user.phone }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
</div>
</div>
</div>
<div style="flex:1;min-width:300px;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Informations entreprise</h2>
<div style="display:flex;flex-direction:column;gap:1rem;">
<div>
<label for="edit_company" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Raison sociale</label>
<input type="text" id="edit_company" name="company_name" value="{{ user.companyName }}" required
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
<div>
<label for="edit_siret" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">SIRET</label>
<input type="text" id="edit_siret" name="siret" value="{{ user.siret }}" maxlength="14" pattern="[0-9]{14}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
<div>
<label for="edit_address" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Adresse</label>
<input type="text" id="edit_address" name="address" value="{{ user.address }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
<div style="display:flex;gap:1rem;">
<div style="flex:1;">
<label for="edit_postal" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Code postal</label>
<input type="text" id="edit_postal" name="postal_code" value="{{ user.postalCode }}" maxlength="5" pattern="[0-9]{5}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
<div style="flex:2;">
<label for="edit_city" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Ville</label>
<input type="text" id="edit_city" name="city" value="{{ user.city }}"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
</div>
</div>
</div>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;margin-bottom:2rem;">
<div style="flex:1;min-width:300px;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:#fabf04;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Offre et commission</h2>
<div style="display:flex;gap:1rem;">
<div style="flex:1;">
<label for="edit_offer" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase">Offre</label>
<select id="edit_offer" name="offer" style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;cursor:pointer;background:white;">
<option value="free" {{ user.offer == 'free' ? 'selected' : '' }}>Gratuit</option>
<option value="basic" {{ user.offer == 'basic' ? 'selected' : '' }}>Basique</option>
<option value="custom" {{ user.offer == 'custom' ? 'selected' : '' }}>Sur-mesure</option>
</select>
</div>
<div style="flex:1;">
<label for="edit_commission" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase">Taux de commission (%)</label>
<input type="number" id="edit_commission" name="commission_rate" value="{{ user.commissionRate ?? 3 }}" step="0.1" min="0" max="100"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;background:white;">
</div>
</div>
</div>
</div>
</div>
<div style="display:flex;gap:0.75rem;">
<button type="submit"
style="padding:0.75rem 1.5rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;cursor:pointer;"
class="font-black uppercase text-sm tracking-widest hover:bg-green-500 hover:text-black transition-all">
Enregistrer
</button>
<a href="{{ path('app_admin_organizers', {tab: 'approved'}) }}"
style="padding:0.75rem 1.5rem;border:3px solid #111827;display:inline-flex;align-items:center;"
class="font-black uppercase text-sm tracking-widest bg-white hover:bg-gray-100 transition-all">
Annuler
</a>
</div>
</form>
{% endblock %}

View File

@@ -61,20 +61,14 @@
{% endif %}
</td>
<td style="padding:0.75rem 1.5rem;text-align:right;">
<div style="display:flex;gap:0.5rem;justify-content:flex-end;">
{% if not orga.approved %}
<form method="post" action="{{ path('app_admin_approve_organizer', {id: orga.id}) }}">
<button type="submit" style="border:2px solid #111827;padding:0.4rem 0.75rem;background:#fabf04;cursor:pointer;" class="text-xs font-black uppercase tracking-widest hover:bg-green-500 hover:text-black transition-all">Approuver</button>
</form>
<form method="post" action="{{ path('app_admin_reject_organizer', {id: orga.id}) }}" data-confirm="Etes-vous sur de vouloir refuser et supprimer le compte de {{ orga.firstName }} {{ orga.lastName }} ? Cette action est irreversible.">
<button type="submit" style="border:2px solid #991b1b;padding:0.4rem 0.75rem;background:#dc2626;color:white;cursor:pointer;" class="text-xs font-black uppercase tracking-widest hover:bg-red-800 transition-all">Refuser</button>
</form>
{% else %}
<form method="post" action="{{ path('app_admin_delete_buyer', {id: orga.id}) }}" data-confirm="Etes-vous sur de vouloir supprimer le compte de {{ orga.firstName }} {{ orga.lastName }} ({{ orga.email }}) ? Cette action est irreversible.">
<button type="submit" style="border:2px solid #991b1b;padding:0.4rem 0.75rem;background:#dc2626;color:white;cursor:pointer;" class="text-xs font-black uppercase tracking-widest hover:bg-red-800 transition-all">Supprimer</button>
</form>
{% endif %}
</div>
{% if not orga.approved %}
<a href="{{ path('app_admin_siret_check', {id: orga.id}) }}" style="border:2px solid #111827;padding:0.4rem 0.75rem;background:#fabf04;display:inline-block;" class="text-xs font-black uppercase tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Voir la demande</a>
{% else %}
<div style="display:flex;gap:0.5rem;justify-content:flex-end;align-items:center;">
<span style="background:#d1fae5;border:2px solid #111827;padding:0.15rem 0.5rem;" class="text-xs font-black uppercase">{{ orga.offer ?? '—' }}{{ orga.commissionRate ?? '3' }}%</span>
<a href="{{ path('app_admin_edit_organizer', {id: orga.id}) }}" style="border:2px solid #111827;padding:0.4rem 0.75rem;background:white;display:inline-block;" class="text-xs font-black uppercase tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Modifier</a>
</div>
{% endif %}
</td>
</tr>
{% else %}

View File

@@ -0,0 +1,261 @@
{% extends 'admin/base.html.twig' %}
{% block title %}Verification Entreprise / Association - {{ siret }}{% endblock %}
{% block body %}
<div style="margin-bottom:2rem;">
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:0.5rem;">Verification Entreprise / Association</h1>
<p class="font-bold text-gray-500 italic">{{ user.firstName }} {{ user.lastName }}{{ siret }}</p>
</div>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;">
<div style="flex:1;min-width:300px;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Informations declarees</h2>
<table style="width:100%;border-collapse:collapse;">
<tbody>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Nom</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ user.firstName }} {{ user.lastName }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Raison sociale</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ user.companyName }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">SIRET</td>
<td style="padding:0.5rem 0;" class="text-sm font-mono">{{ user.siret }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Adresse</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ user.address }}, {{ user.postalCode }} {{ user.city }}</td>
</tr>
<tr>
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Telephone</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ user.phone }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div style="flex:1;min-width:300px;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Donnees API Gouvernement</h2>
{% if data %}
<div style="margin-bottom:1rem;display:flex;gap:0.5rem;flex-wrap:wrap;">
{% if data.complements is defined and data.complements.est_association is defined and data.complements.est_association %}
<span style="background:#dbeafe;border:2px solid #111827;padding:0.15rem 0.75rem;" class="text-xs font-black uppercase">Association</span>
{% elseif data.complements is defined and data.complements.est_entrepreneur_individuel is defined and data.complements.est_entrepreneur_individuel %}
<span style="background:#fef3c7;border:2px solid #111827;padding:0.15rem 0.75rem;" class="text-xs font-black uppercase">Entrepreneur individuel</span>
{% else %}
<span style="background:#e0e7ff;border:2px solid #111827;padding:0.15rem 0.75rem;" class="text-xs font-black uppercase">Entreprise</span>
{% endif %}
{% if data.complements is defined and data.complements.est_ess is defined and data.complements.est_ess %}
<span style="background:#d1fae5;border:2px solid #111827;padding:0.15rem 0.75rem;" class="text-xs font-black uppercase">ESS</span>
{% endif %}
</div>
<table style="width:100%;border-collapse:collapse;">
<tbody>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Denomination</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ data.nom_complet ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">SIREN</td>
<td style="padding:0.5rem 0;" class="text-sm font-mono">{{ data.siren ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Nature juridique</td>
<td style="padding:0.5rem 0;" class="text-sm">
{% if data.nature_juridique is defined and data.nature_juridique %}
<span class="font-mono">{{ data.nature_juridique }}</span> — {{ data.libelle_nature_juridique ?? '' }}
{% else %}
{% endif %}
</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Siege</td>
<td style="padding:0.5rem 0;" class="text-sm">
{% if data.siege is defined and data.siege %}
{{ data.siege.adresse ?? '' }}, {{ data.siege.code_postal ?? '' }} {{ data.siege.libelle_commune ?? '' }}
{% else %}
{% endif %}
</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Activite (NAF)</td>
<td style="padding:0.5rem 0;" class="text-sm">
{% if data.activite_principale is defined and data.activite_principale %}
<span class="font-mono">{{ data.activite_principale }}</span> — {{ data.libelle_activite_principale ?? '' }}
{% else %}
{% endif %}
</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Tranche effectif</td>
<td style="padding:0.5rem 0;" class="text-sm">
{% set te = data.tranche_effectif_salarie ?? 'NN' %}
{% set te_labels = {
'NN': 'Non renseigne',
'00': '0 salarie',
'01': '1 ou 2 salaries',
'02': '3 a 5 salaries',
'03': '6 a 9 salaries',
'11': '10 a 19 salaries',
'12': '20 a 49 salaries',
'21': '50 a 99 salaries',
'22': '100 a 199 salaries',
'31': '200 a 249 salaries',
'32': '250 a 499 salaries',
'41': '500 a 999 salaries',
'42': '1 000 a 1 999 salaries',
'51': '2 000 a 4 999 salaries',
'52': '5 000 a 9 999 salaries',
'53': '10 000 salaries et plus'
} %}
{{ te_labels[te] ?? te }}
</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Date creation</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ data.date_creation ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Statut</td>
<td style="padding:0.5rem 0;">
{% if data.etat_administratif is defined and data.etat_administratif == 'A' %}
<span style="background:#d1fae5;border:2px solid #111827;padding:0.15rem 0.5rem;" class="text-xs font-black uppercase">Active</span>
{% else %}
<span style="background:#fee2e2;border:2px solid #111827;padding:0.15rem 0.5rem;" class="text-xs font-black uppercase">Fermee</span>
{% endif %}
</td>
</tr>
{% if data.complements is defined and data.complements.est_association is defined and data.complements.est_association %}
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">RNA</td>
<td style="padding:0.5rem 0;" class="text-sm font-mono">{{ data.complements.identifiant_association ?? '—' }}</td>
</tr>
{% endif %}
<tr>
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Coordonnees</td>
<td style="padding:0.5rem 0;" class="text-sm">
{% if data.siege is defined and data.siege.latitude is defined and data.siege.longitude is defined and data.siege.latitude and data.siege.longitude %}
<span class="font-mono">{{ data.siege.latitude }}, {{ data.siege.longitude }}</span>
{% else %}
{% endif %}
</td>
</tr>
</tbody>
</table>
{% else %}
<div style="padding:2rem;text-align:center;">
<span style="background:#fee2e2;border:2px solid #111827;padding:0.25rem 0.75rem;" class="text-xs font-black uppercase">SIRET non trouve</span>
<p class="text-gray-400 text-sm font-bold" style="margin-top:0.5rem;">Aucun resultat retourne par l'API pour ce SIRET.</p>
</div>
{% endif %}
</div>
</div>
</div>
{% if rnaData %}
<div style="margin-top:1.5rem;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Journal Officiel (JOAFE)</h2>
<table style="width:100%;border-collapse:collapse;">
<tbody>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;width:180px;" class="font-bold text-sm text-gray-400">RNA</td>
<td style="padding:0.5rem 0;" class="text-sm font-mono">{{ rnaData.numero_rna ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Regime</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ rnaData.association_type_libelle ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;vertical-align:top;" class="font-bold text-sm text-gray-400">Objet</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ rnaData.objet ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Domaine d'activite</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ rnaData.domaine_activite_libelle_categorise ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Date declaration</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ rnaData.datedeclaration ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Date parution JOAFE</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ rnaData.dateparution ?? '—' }}</td>
</tr>
<tr style="border-bottom:1px solid #e5e7eb;">
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">Lieu declaration</td>
<td style="padding:0.5rem 0;" class="text-sm">{{ rnaData.lieupref ?? '—' }}</td>
</tr>
<tr>
<td style="padding:0.5rem 0;" class="font-bold text-sm text-gray-400">PDF JOAFE</td>
<td style="padding:0.5rem 0;">
{% if rnaData.url_pdf is defined and rnaData.url_pdf %}
<a href="{{ rnaData.url_pdf }}" target="_blank" style="border:2px solid #111827;padding:0.15rem 0.5rem;background:#fabf04;display:inline-block;" class="text-xs font-black uppercase hover:bg-indigo-600 hover:text-black transition-all">Telecharger</a>
{% else %}
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endif %}
<div style="margin-top:2rem;display:flex;gap:0.75rem;flex-wrap:wrap;">
<form method="post" action="{{ path('app_admin_siret_refresh', {id: user.id}) }}" style="display:inline;">
<button type="submit" style="padding:0.5rem 1rem;border:3px solid #111827;background:white;cursor:pointer;" class="font-black uppercase text-xs tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Rafraichir SIRET</button>
</form>
<a href="{{ path('app_admin_organizers') }}" style="padding:0.5rem 1rem;border:3px solid #111827;background:white;display:inline-block;" class="font-black uppercase text-xs tracking-widest hover:bg-gray-100 transition-all">Retour</a>
</div>
<div style="display:flex;flex-wrap:wrap;gap:1.5rem;margin-top:2rem;">
<div style="flex:1;min-width:300px;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;color:#16a34a;">Approuver l'organisateur</h2>
<form method="post" action="{{ path('app_admin_approve_organizer', {id: user.id}) }}" style="display:flex;flex-direction:column;gap:1rem;">
<div>
<label for="approve_offer" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.5rem;" class="font-black uppercase text-gray-400">Offre</label>
<select id="approve_offer" name="offer" style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;cursor:pointer;">
<option value="free">Gratuit</option>
<option value="basic">Basique</option>
<option value="custom">Sur-mesure</option>
</select>
</div>
<div>
<label for="approve_commission" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.5rem;" class="font-black uppercase text-gray-400">Taux de commission (%)</label>
<input type="number" id="approve_commission" name="commission_rate" value="3" step="0.1" min="0" max="100"
style="width:100%;padding:0.5rem 0.75rem;border:3px solid #111827;font-weight:700;outline:none;">
</div>
<button type="submit" style="padding:0.5rem 1rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;cursor:pointer;" class="font-black uppercase text-xs tracking-widest hover:bg-green-500 hover:text-black transition-all">Approuver l'organisateur</button>
</form>
</div>
</div>
<div style="flex:1;min-width:300px;">
<div style="border:4px solid #991b1b;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:1.5rem;">
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;color:#991b1b;">Refuser la demande</h2>
<form method="post" action="{{ path('app_admin_reject_organizer', {id: user.id}) }}" data-confirm="Etes-vous sur de vouloir refuser et supprimer le compte de {{ user.firstName }} {{ user.lastName }} ? Cette action est irreversible." style="display:flex;flex-direction:column;gap:1rem;">
<div>
<label for="reject_reason" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.5rem;" class="font-black uppercase text-gray-400">Motif du refus</label>
<textarea id="reject_reason" name="reason" required rows="4"
style="width:100%;padding:0.75rem 1rem;border:3px solid #111827;font-weight:700;outline:none;resize:vertical;"
placeholder="Expliquez la raison du refus..."></textarea>
</div>
<button type="submit" style="padding:0.5rem 1rem;border:3px solid #991b1b;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#dc2626;color:white;cursor:pointer;" class="font-black uppercase text-xs tracking-widest hover:bg-red-800 transition-all">Refuser et supprimer le compte</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@@ -5,6 +5,12 @@
{% block content %}
<h2>Bonjour {{ firstName }},</h2>
<p>Nous avons examine votre demande de compte organisateur et malheureusement, celle-ci a ete <strong>refusee</strong>.</p>
{% if reason is defined and reason %}
<div style="background:#f4f4f5;border-left:4px solid #dc2626;padding:12px 16px;margin:16px 0;">
<p style="font-weight:700;font-size:13px;color:#991b1b;margin:0 0 4px;">Motif du refus :</p>
<p style="margin:0;font-size:14px;color:#3f3f46;">{{ reason }}</p>
</div>
{% endif %}
<p>Si vous pensez qu'il s'agit d'une erreur ou si vous souhaitez obtenir plus d'informations, n'hesitez pas a nous contacter.</p>
<p style="text-align:center;margin:32px 0;">
<a href="mailto:contact@e-cosplay.fr" class="btn">Nous contacter</a>

View File

@@ -0,0 +1,26 @@
{% extends 'base.html.twig' %}
{% block title %}Email verifie - E-Ticket{% endblock %}
{% block body %}
<div style="max-width:36rem;margin:0 auto;padding:3rem 1rem;text-align:center;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:2.5rem;">
<div style="font-size:3rem;margin-bottom:1rem;">&#10003;</div>
<h1 class="text-2xl font-black uppercase tracking-tighter italic" style="margin-bottom:1rem;">Email verifie !</h1>
<div style="border:4px solid #111827;background:#d1fae5;padding:1rem 1.5rem;margin-bottom:1.5rem;">
<p class="font-bold text-sm">Votre adresse email a ete verifiee avec succes.</p>
</div>
{% if isOrganizer %}
<div style="border:4px solid #111827;background:#fabf04;padding:1rem 1.5rem;margin-bottom:1.5rem;">
<p class="font-bold text-sm">L'equipe E-Ticket va maintenant examiner votre demande de compte organisateur. Vous recevrez une reponse sous 48h.</p>
</div>
{% else %}
<p class="font-bold text-gray-500 text-sm" style="margin-bottom:1.5rem;">Votre compte est actif. Vous pouvez maintenant vous connecter.</p>
{% endif %}
<a href="{{ path('app_login') }}" style="display:inline-block;padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;" class="font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Se connecter</a>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'base.html.twig' %}
{% block title %}Compte cree - E-Ticket{% endblock %}
{% block body %}
<div style="max-width:36rem;margin:0 auto;padding:3rem 1rem;text-align:center;">
<div style="border:4px solid #111827;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;padding:2.5rem;">
<div style="font-size:3rem;margin-bottom:1rem;">&#10003;</div>
<h1 class="text-2xl font-black uppercase tracking-tighter italic" style="margin-bottom:1rem;">Compte cree avec succes !</h1>
<div style="border:4px solid #111827;background:#d1fae5;padding:1rem 1.5rem;margin-bottom:1.5rem;">
<p class="font-bold text-sm">Un email de verification vous a ete envoye. Veuillez cliquer sur le lien dans l'email pour activer votre compte.</p>
</div>
{% if isOrganizer %}
<div style="border:4px solid #111827;background:#fabf04;padding:1rem 1.5rem;margin-bottom:1.5rem;">
<p class="font-bold text-sm">Une fois votre email verifie, l'equipe E-Ticket examinera votre demande de compte organisateur et vous donnera une reponse sous 48h.</p>
</div>
{% endif %}
<a href="{{ path('app_login') }}" style="display:inline-block;padding:0.75rem 2rem;border:3px solid #111827;box-shadow:4px 4px 0 rgba(0,0,0,1);background:#fabf04;" class="font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Aller a la connexion</a>
</div>
</div>
{% endblock %}

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'));
}
}