Les administrateurs et secrétaires avaient besoin de pouvoir inscrire un élève en cours d'année sans passer par un import CSV. Cette fonctionnalité pose aussi les fondations du modèle élève↔classe (ClassAssignment) qui sera réutilisé par l'import CSV en masse (Story 3.1). L'email est désormais optionnel pour les élèves : si fourni, une invitation est envoyée (User::inviter) ; sinon l'élève est créé avec le statut INSCRIT sans accès compte (User::inscrire). La création de l'utilisateur et l'affectation à la classe sont atomiques (transaction DBAL). Côté frontend, la page /admin/students offre liste paginée, recherche, filtrage par classe, création via modale (avec détection de doublons côté serveur), et changement de classe avec optimistic update.
129 lines
5.1 KiB
PHP
129 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Domain\Model\ClassAssignment;
|
|
|
|
use App\Administration\Domain\Event\EleveAffecteAClasse;
|
|
use App\Administration\Domain\Model\ClassAssignment\ClassAssignment;
|
|
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
|
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ClassAssignmentTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440010';
|
|
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
|
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440040';
|
|
|
|
#[Test]
|
|
public function affecterCreeAffectationAvecIdUnique(): void
|
|
{
|
|
$assignment = $this->createAssignment();
|
|
|
|
self::assertNotEmpty((string) $assignment->id);
|
|
self::assertTrue($assignment->tenantId->equals(TenantId::fromString(self::TENANT_ID)));
|
|
self::assertTrue($assignment->studentId->equals(UserId::fromString(self::STUDENT_ID)));
|
|
self::assertTrue($assignment->classId->equals(ClassId::fromString(self::CLASS_ID)));
|
|
self::assertTrue($assignment->academicYearId->equals(AcademicYearId::fromString(self::ACADEMIC_YEAR_ID)));
|
|
}
|
|
|
|
#[Test]
|
|
public function affecterEnregistreEvenementEleveAffecteAClasse(): void
|
|
{
|
|
$assignment = $this->createAssignment();
|
|
|
|
$events = $assignment->pullDomainEvents();
|
|
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(EleveAffecteAClasse::class, $events[0]);
|
|
self::assertTrue($events[0]->studentId->equals(UserId::fromString(self::STUDENT_ID)));
|
|
self::assertTrue($events[0]->classId->equals(ClassId::fromString(self::CLASS_ID)));
|
|
}
|
|
|
|
#[Test]
|
|
public function affecterInitialiseUpdatedAtAvecCreatedAt(): void
|
|
{
|
|
$now = new DateTimeImmutable('2026-02-21 10:00:00');
|
|
|
|
$assignment = ClassAssignment::affecter(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
assignedAt: $now,
|
|
);
|
|
|
|
self::assertSame($now, $assignment->createdAt);
|
|
self::assertSame($now, $assignment->updatedAt);
|
|
self::assertSame($now, $assignment->assignedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function changerClasseModifieLaClasseEtUpdatedAt(): void
|
|
{
|
|
$assignment = $this->createAssignment();
|
|
$assignment->pullDomainEvents(); // Clear creation event
|
|
|
|
$newClassId = ClassId::fromString('550e8400-e29b-41d4-a716-446655440099');
|
|
$changedAt = new DateTimeImmutable('2026-03-15 14:00:00');
|
|
|
|
$assignment->changerClasse($newClassId, $changedAt);
|
|
|
|
self::assertTrue($assignment->classId->equals($newClassId));
|
|
self::assertSame($changedAt, $assignment->updatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function changerClasseEnregistreEvenementEleveAffecteAClasse(): void
|
|
{
|
|
$assignment = $this->createAssignment();
|
|
$assignment->pullDomainEvents(); // Clear creation event
|
|
|
|
$newClassId = ClassId::fromString('550e8400-e29b-41d4-a716-446655440099');
|
|
$changedAt = new DateTimeImmutable('2026-03-15 14:00:00');
|
|
|
|
$assignment->changerClasse($newClassId, $changedAt);
|
|
|
|
$events = $assignment->pullDomainEvents();
|
|
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(EleveAffecteAClasse::class, $events[0]);
|
|
self::assertTrue($events[0]->classId->equals($newClassId));
|
|
self::assertSame($changedAt, $events[0]->occurredOn());
|
|
}
|
|
|
|
#[Test]
|
|
public function reconstituteNeGenereAucunEvenement(): void
|
|
{
|
|
$assignment = ClassAssignment::reconstitute(
|
|
id: \App\Administration\Domain\Model\ClassAssignment\ClassAssignmentId::fromString('550e8400-e29b-41d4-a716-446655440050'),
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
assignedAt: new DateTimeImmutable('2026-02-21 10:00:00'),
|
|
createdAt: new DateTimeImmutable('2026-02-21 10:00:00'),
|
|
updatedAt: new DateTimeImmutable('2026-02-21 12:00:00'),
|
|
);
|
|
|
|
self::assertEmpty($assignment->pullDomainEvents());
|
|
}
|
|
|
|
private function createAssignment(): ClassAssignment
|
|
{
|
|
return ClassAssignment::affecter(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
|
assignedAt: new DateTimeImmutable('2026-02-21 10:00:00'),
|
|
);
|
|
}
|
|
}
|