Permet aux administrateurs de créer, modifier et supprimer des classes pour organiser les élèves par niveau. L'archivage soft-delete préserve l'historique tout en masquant les classes obsolètes. Inclut la validation des noms (2-50 caractères), les niveaux scolaires du CP à la Terminale, et les contrôles d'accès par rôle.
136 lines
4.9 KiB
PHP
136 lines
4.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Application\Command\ArchiveClass;
|
|
|
|
use App\Administration\Application\Command\ArchiveClass\ArchiveClassCommand;
|
|
use App\Administration\Application\Command\ArchiveClass\ArchiveClassHandler;
|
|
use App\Administration\Application\Query\HasStudentsInClass\HasStudentsInClassQuery;
|
|
use App\Administration\Domain\Exception\ClasseNonSupprimableException;
|
|
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
|
use App\Administration\Domain\Model\SchoolClass\ClassName;
|
|
use App\Administration\Domain\Model\SchoolClass\ClassStatus;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolClass;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolId;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolLevel;
|
|
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryClassRepository;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
use RuntimeException;
|
|
use Symfony\Component\Messenger\Envelope;
|
|
use Symfony\Component\Messenger\MessageBusInterface;
|
|
use Symfony\Component\Messenger\Stamp\HandledStamp;
|
|
|
|
final class ArchiveClassHandlerTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440002';
|
|
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440003';
|
|
|
|
private InMemoryClassRepository $classRepository;
|
|
private Clock $clock;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->classRepository = new InMemoryClassRepository();
|
|
$this->clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-02-01 10:00:00');
|
|
}
|
|
};
|
|
}
|
|
|
|
#[Test]
|
|
public function itArchivesEmptyClass(): void
|
|
{
|
|
$class = $this->createAndSaveClass();
|
|
$queryBus = $this->createQueryBusReturning(0);
|
|
$handler = new ArchiveClassHandler($this->classRepository, $queryBus, $this->clock);
|
|
|
|
$command = new ArchiveClassCommand(classId: (string) $class->id);
|
|
|
|
$handler($command);
|
|
|
|
$archivedClass = $this->classRepository->get($class->id);
|
|
self::assertSame(ClassStatus::ARCHIVED, $archivedClass->status);
|
|
self::assertNotNull($archivedClass->deletedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsExceptionWhenStudentsAreAffected(): void
|
|
{
|
|
$class = $this->createAndSaveClass();
|
|
$queryBus = $this->createQueryBusReturning(5);
|
|
$handler = new ArchiveClassHandler($this->classRepository, $queryBus, $this->clock);
|
|
|
|
$command = new ArchiveClassCommand(classId: (string) $class->id);
|
|
|
|
$this->expectException(ClasseNonSupprimableException::class);
|
|
$this->expectExceptionMessage('5 élève(s)');
|
|
|
|
$handler($command);
|
|
}
|
|
|
|
#[Test]
|
|
public function itDoesNotModifyStatusWhenAlreadyArchived(): void
|
|
{
|
|
$class = $this->createAndSaveClass();
|
|
$archiveTime = new DateTimeImmutable('2026-01-20 10:00:00');
|
|
$class->archiver($archiveTime);
|
|
$this->classRepository->save($class);
|
|
|
|
$queryBus = $this->createQueryBusReturning(0);
|
|
$handler = new ArchiveClassHandler($this->classRepository, $queryBus, $this->clock);
|
|
|
|
$command = new ArchiveClassCommand(classId: (string) $class->id);
|
|
|
|
$handler($command);
|
|
|
|
$archivedClass = $this->classRepository->get($class->id);
|
|
// deletedAt should not have changed
|
|
self::assertEquals($archiveTime, $archivedClass->deletedAt);
|
|
}
|
|
|
|
private function createAndSaveClass(): SchoolClass
|
|
{
|
|
$class = SchoolClass::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
name: new ClassName('6ème A'),
|
|
level: SchoolLevel::SIXIEME,
|
|
capacity: 30,
|
|
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
|
|
);
|
|
|
|
$this->classRepository->save($class);
|
|
|
|
return $class;
|
|
}
|
|
|
|
private function createQueryBusReturning(int $studentCount): MessageBusInterface
|
|
{
|
|
return new class($studentCount) implements MessageBusInterface {
|
|
public function __construct(private readonly int $studentCount)
|
|
{
|
|
}
|
|
|
|
public function dispatch(object $message, array $stamps = []): Envelope
|
|
{
|
|
if (!$message instanceof HasStudentsInClassQuery) {
|
|
throw new RuntimeException('Unexpected message type');
|
|
}
|
|
|
|
$envelope = new Envelope($message);
|
|
|
|
return $envelope->with(new HandledStamp($this->studentCount, 'handler'));
|
|
}
|
|
};
|
|
}
|
|
}
|