feat: Permettre la génération et l'envoi de codes d'invitation aux parents

Les administrateurs ont besoin d'un moyen simple pour inviter les parents
à rejoindre la plateforme. Cette fonctionnalité permet de générer des codes
d'invitation uniques (8 caractères alphanumériques) avec une validité de
48h, de les envoyer par email, et de les activer via une page publique
dédiée qui crée automatiquement le compte parent.

L'interface d'administration offre l'envoi unitaire et en masse, le renvoi,
le filtrage par statut, ainsi que la visualisation de l'état de chaque
invitation (en attente, activée, expirée).
This commit is contained in:
2026-02-28 00:08:56 +01:00
parent de5880e25e
commit be1b0b60a6
68 changed files with 8787 additions and 1 deletions

View File

@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace App\Administration\Infrastructure\Console;
use App\Administration\Domain\Repository\ParentInvitationRepository;
use App\Shared\Domain\Clock;
use function count;
use Override;
use Psr\Log\LoggerInterface;
use function sprintf;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;
/**
* Marque comme expirées les invitations parents envoyées dont la date d'expiration est dépassée.
*
* CRON: 0 6 * * * php bin/console app:expire-parent-invitations
*/
#[AsCommand(
name: 'app:expire-parent-invitations',
description: 'Marque comme expirées les invitations parents dont la date limite est dépassée',
)]
final class ExpireInvitationsCommand extends Command
{
public function __construct(
private readonly ParentInvitationRepository $invitationRepository,
private readonly Clock $clock,
private readonly LoggerInterface $logger,
) {
parent::__construct();
}
#[Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Expiration des invitations parents');
$now = $this->clock->now();
$expiredInvitations = $this->invitationRepository->findExpiredSent($now);
if ($expiredInvitations === []) {
$io->success('Aucune invitation expirée à traiter.');
return Command::SUCCESS;
}
$io->info(sprintf('%d invitation(s) expirée(s) trouvée(s)', count($expiredInvitations)));
$expiredCount = 0;
foreach ($expiredInvitations as $invitation) {
try {
$invitation->marquerExpiree();
$this->invitationRepository->save($invitation);
$this->logger->info('Invitation parent marquée expirée', [
'invitation_id' => (string) $invitation->id,
'tenant_id' => (string) $invitation->tenantId,
'parent_email' => (string) $invitation->parentEmail,
]);
++$expiredCount;
} catch (Throwable $e) {
$io->error(sprintf(
'Erreur pour l\'invitation %s : %s',
$invitation->id,
$e->getMessage(),
));
$this->logger->error('Erreur lors de l\'expiration de l\'invitation', [
'invitation_id' => (string) $invitation->id,
'error' => $e->getMessage(),
]);
}
}
$io->success(sprintf('%d invitation(s) marquée(s) comme expirée(s).', $expiredCount));
return Command::SUCCESS;
}
}