feat: Gestion des classes scolaires

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.
This commit is contained in:
2026-02-05 15:24:29 +01:00
parent b45ef735db
commit 8e09e0abf1
54 changed files with 5099 additions and 5 deletions

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Infrastructure\Persistence\InMemory;
use App\Administration\Domain\Exception\ClasseNotFoundException;
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\SchoolClass\ClassName;
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\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class InMemoryClassRepositoryTest 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 $repository;
protected function setUp(): void
{
$this->repository = new InMemoryClassRepository();
}
#[Test]
public function saveAndGet(): void
{
$class = $this->createClass('6ème A');
$this->repository->save($class);
$retrieved = $this->repository->get($class->id);
self::assertTrue($class->id->equals($retrieved->id));
self::assertTrue($class->name->equals($retrieved->name));
}
#[Test]
public function getThrowsExceptionForUnknownId(): void
{
$this->expectException(ClasseNotFoundException::class);
$this->repository->get(ClassId::generate());
}
#[Test]
public function findByIdReturnsNullForUnknownId(): void
{
$result = $this->repository->findById(ClassId::generate());
self::assertNull($result);
}
#[Test]
public function findByName(): void
{
$class = $this->createClass('6ème A');
$this->repository->save($class);
$found = $this->repository->findByName(
new ClassName('6ème A'),
TenantId::fromString(self::TENANT_ID),
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
);
self::assertNotNull($found);
self::assertTrue($class->id->equals($found->id));
}
#[Test]
public function findByNameReturnsNullForUnknownName(): void
{
$class = $this->createClass('6ème A');
$this->repository->save($class);
$found = $this->repository->findByName(
new ClassName('6ème B'),
TenantId::fromString(self::TENANT_ID),
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
);
self::assertNull($found);
}
#[Test]
public function findByNameIsCaseInsensitive(): void
{
$class = $this->createClass('6ème A');
$this->repository->save($class);
$found = $this->repository->findByName(
new ClassName('6ÈME A'),
TenantId::fromString(self::TENANT_ID),
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
);
self::assertNotNull($found);
}
#[Test]
public function findActiveByTenantAndYear(): void
{
$class1 = $this->createClass('6ème A');
$class2 = $this->createClass('6ème B');
$class3 = $this->createClass('6ème C');
$class3->archiver(new DateTimeImmutable());
$this->repository->save($class1);
$this->repository->save($class2);
$this->repository->save($class3);
$activeClasses = $this->repository->findActiveByTenantAndYear(
TenantId::fromString(self::TENANT_ID),
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
);
self::assertCount(2, $activeClasses);
}
#[Test]
public function findActiveByTenantAndYearReturnsEmptyArrayForDifferentTenant(): void
{
$class = $this->createClass('6ème A');
$this->repository->save($class);
$activeClasses = $this->repository->findActiveByTenantAndYear(
TenantId::fromString('550e8400-e29b-41d4-a716-446655440099'),
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
);
self::assertEmpty($activeClasses);
}
#[Test]
public function delete(): void
{
$class = $this->createClass('6ème A');
$this->repository->save($class);
$this->repository->delete($class->id);
self::assertNull($this->repository->findById($class->id));
}
#[Test]
public function deleteNonExistentClassDoesNotThrow(): void
{
// Should not throw
$this->repository->delete(ClassId::generate());
$this->expectNotToPerformAssertions();
}
private function createClass(string $name): SchoolClass
{
return SchoolClass::creer(
tenantId: TenantId::fromString(self::TENANT_ID),
schoolId: SchoolId::fromString(self::SCHOOL_ID),
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
name: new ClassName($name),
level: SchoolLevel::SIXIEME,
capacity: 30,
createdAt: new DateTimeImmutable('2026-01-15 10:00:00'),
);
}
}