Les administrateurs devaient recréer manuellement l'emploi du temps chaque semaine. Cette implémentation introduit un système de récurrence hebdomadaire avec gestion des exceptions par occurrence, permettant de modifier ou annuler un cours spécifique sans affecter les autres semaines. Le ScheduleResolver calcule dynamiquement l'EDT réel en combinant les créneaux récurrents, les exceptions ponctuelles et le calendrier scolaire (vacances/fériés).
369 lines
13 KiB
PHP
369 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Domain\Model\Schedule;
|
|
|
|
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
|
use App\Administration\Domain\Model\Subject\SubjectId;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Scolarite\Domain\Event\CoursCree;
|
|
use App\Scolarite\Domain\Event\CoursModifie;
|
|
use App\Scolarite\Domain\Event\CoursSupprime;
|
|
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
|
|
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
|
|
use App\Scolarite\Domain\Model\Schedule\ScheduleSlotId;
|
|
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class ScheduleSlotTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
|
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
|
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
|
|
|
#[Test]
|
|
public function creerCreatesSlotWithCorrectProperties(): void
|
|
{
|
|
$slot = $this->createSlot();
|
|
|
|
self::assertTrue($slot->tenantId->equals(TenantId::fromString(self::TENANT_ID)));
|
|
self::assertTrue($slot->classId->equals(ClassId::fromString(self::CLASS_ID)));
|
|
self::assertTrue($slot->subjectId->equals(SubjectId::fromString(self::SUBJECT_ID)));
|
|
self::assertTrue($slot->teacherId->equals(UserId::fromString(self::TEACHER_ID)));
|
|
self::assertSame(DayOfWeek::MONDAY, $slot->dayOfWeek);
|
|
self::assertSame('08:00', $slot->timeSlot->startTime);
|
|
self::assertSame('09:00', $slot->timeSlot->endTime);
|
|
self::assertNull($slot->room);
|
|
self::assertTrue($slot->isRecurring);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerRecordsCoursCreeEvent(): void
|
|
{
|
|
$slot = $this->createSlot();
|
|
|
|
$events = $slot->pullDomainEvents();
|
|
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(CoursCree::class, $events[0]);
|
|
self::assertTrue($slot->id->equals($events[0]->slotId));
|
|
}
|
|
|
|
#[Test]
|
|
public function creerWithRoomSetsRoom(): void
|
|
{
|
|
$slot = $this->createSlot(room: 'Salle 101');
|
|
|
|
self::assertSame('Salle 101', $slot->room);
|
|
}
|
|
|
|
#[Test]
|
|
public function modifierUpdatesProperties(): void
|
|
{
|
|
$slot = $this->createSlot();
|
|
$slot->pullDomainEvents();
|
|
|
|
$newSubjectId = SubjectId::fromString('550e8400-e29b-41d4-a716-446655440031');
|
|
$newTeacherId = UserId::fromString('550e8400-e29b-41d4-a716-446655440011');
|
|
$newTimeSlot = new TimeSlot('10:00', '11:00');
|
|
$now = new DateTimeImmutable('2026-03-02 15:00:00');
|
|
|
|
$newClassId = ClassId::fromString('550e8400-e29b-41d4-a716-446655440021');
|
|
|
|
$slot->modifier(
|
|
classId: $newClassId,
|
|
subjectId: $newSubjectId,
|
|
teacherId: $newTeacherId,
|
|
dayOfWeek: DayOfWeek::TUESDAY,
|
|
timeSlot: $newTimeSlot,
|
|
room: 'Salle 202',
|
|
at: $now,
|
|
);
|
|
|
|
self::assertTrue($slot->classId->equals($newClassId));
|
|
self::assertTrue($slot->subjectId->equals($newSubjectId));
|
|
self::assertTrue($slot->teacherId->equals($newTeacherId));
|
|
self::assertSame(DayOfWeek::TUESDAY, $slot->dayOfWeek);
|
|
self::assertSame('10:00', $slot->timeSlot->startTime);
|
|
self::assertSame('11:00', $slot->timeSlot->endTime);
|
|
self::assertSame('Salle 202', $slot->room);
|
|
self::assertEquals($now, $slot->updatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function modifierRecordsCoursModifieEvent(): void
|
|
{
|
|
$slot = $this->createSlot();
|
|
$slot->pullDomainEvents();
|
|
|
|
$now = new DateTimeImmutable('2026-03-02 15:00:00');
|
|
$slot->modifier(
|
|
classId: $slot->classId,
|
|
subjectId: $slot->subjectId,
|
|
teacherId: $slot->teacherId,
|
|
dayOfWeek: DayOfWeek::TUESDAY,
|
|
timeSlot: new TimeSlot('10:00', '11:00'),
|
|
room: null,
|
|
at: $now,
|
|
);
|
|
|
|
$events = $slot->pullDomainEvents();
|
|
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(CoursModifie::class, $events[0]);
|
|
self::assertTrue($slot->id->equals($events[0]->slotId));
|
|
}
|
|
|
|
#[Test]
|
|
public function supprimerRecordsCoursSupprime(): void
|
|
{
|
|
$slot = $this->createSlot();
|
|
$slot->pullDomainEvents();
|
|
|
|
$now = new DateTimeImmutable('2026-03-02 15:00:00');
|
|
$slot->supprimer($now);
|
|
|
|
$events = $slot->pullDomainEvents();
|
|
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(CoursSupprime::class, $events[0]);
|
|
self::assertTrue($slot->id->equals($events[0]->slotId));
|
|
}
|
|
|
|
#[Test]
|
|
public function reconstituteRestoresAllPropertiesWithoutEvents(): void
|
|
{
|
|
$id = ScheduleSlotId::generate();
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
$classId = ClassId::fromString(self::CLASS_ID);
|
|
$subjectId = SubjectId::fromString(self::SUBJECT_ID);
|
|
$teacherId = UserId::fromString(self::TEACHER_ID);
|
|
$timeSlot = new TimeSlot('14:00', '15:30');
|
|
$createdAt = new DateTimeImmutable('2026-03-01 10:00:00');
|
|
$updatedAt = new DateTimeImmutable('2026-03-02 14:00:00');
|
|
|
|
$slot = ScheduleSlot::reconstitute(
|
|
id: $id,
|
|
tenantId: $tenantId,
|
|
classId: $classId,
|
|
subjectId: $subjectId,
|
|
teacherId: $teacherId,
|
|
dayOfWeek: DayOfWeek::FRIDAY,
|
|
timeSlot: $timeSlot,
|
|
room: 'Salle 305',
|
|
isRecurring: false,
|
|
createdAt: $createdAt,
|
|
updatedAt: $updatedAt,
|
|
);
|
|
|
|
self::assertTrue($slot->id->equals($id));
|
|
self::assertTrue($slot->tenantId->equals($tenantId));
|
|
self::assertTrue($slot->classId->equals($classId));
|
|
self::assertTrue($slot->subjectId->equals($subjectId));
|
|
self::assertTrue($slot->teacherId->equals($teacherId));
|
|
self::assertSame(DayOfWeek::FRIDAY, $slot->dayOfWeek);
|
|
self::assertSame('14:00', $slot->timeSlot->startTime);
|
|
self::assertSame('15:30', $slot->timeSlot->endTime);
|
|
self::assertSame('Salle 305', $slot->room);
|
|
self::assertFalse($slot->isRecurring);
|
|
self::assertEquals($createdAt, $slot->createdAt);
|
|
self::assertEquals($updatedAt, $slot->updatedAt);
|
|
self::assertEmpty($slot->pullDomainEvents());
|
|
}
|
|
|
|
#[Test]
|
|
public function creerWithRecurrenceBoundsSetsDates(): void
|
|
{
|
|
$recurrenceStart = new DateTimeImmutable('2026-09-01');
|
|
$recurrenceEnd = new DateTimeImmutable('2027-07-04');
|
|
|
|
$slot = ScheduleSlot::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
timeSlot: new TimeSlot('08:00', '09:00'),
|
|
room: null,
|
|
isRecurring: true,
|
|
now: new DateTimeImmutable('2026-09-01 10:00:00'),
|
|
recurrenceStart: $recurrenceStart,
|
|
recurrenceEnd: $recurrenceEnd,
|
|
);
|
|
|
|
self::assertEquals($recurrenceStart, $slot->recurrenceStart);
|
|
self::assertEquals($recurrenceEnd, $slot->recurrenceEnd);
|
|
}
|
|
|
|
#[Test]
|
|
public function creerWithoutRecurrenceBoundsDefaultsToNull(): void
|
|
{
|
|
$slot = $this->createSlot();
|
|
|
|
self::assertNull($slot->recurrenceStart);
|
|
self::assertNull($slot->recurrenceEnd);
|
|
}
|
|
|
|
#[Test]
|
|
public function isActiveOnDateReturnsTrueForMatchingDayWithinBounds(): void
|
|
{
|
|
$slot = $this->createRecurringSlot(
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
recurrenceStart: new DateTimeImmutable('2026-09-01'),
|
|
recurrenceEnd: new DateTimeImmutable('2027-07-04'),
|
|
);
|
|
|
|
// 2026-09-07 is a Monday within bounds
|
|
self::assertTrue($slot->isActiveOnDate(new DateTimeImmutable('2026-09-07')));
|
|
}
|
|
|
|
#[Test]
|
|
public function isActiveOnDateReturnsFalseForWrongDayOfWeek(): void
|
|
{
|
|
$slot = $this->createRecurringSlot(
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
recurrenceStart: new DateTimeImmutable('2026-09-01'),
|
|
recurrenceEnd: new DateTimeImmutable('2027-07-04'),
|
|
);
|
|
|
|
// 2026-09-08 is a Tuesday
|
|
self::assertFalse($slot->isActiveOnDate(new DateTimeImmutable('2026-09-08')));
|
|
}
|
|
|
|
#[Test]
|
|
public function isActiveOnDateReturnsFalseBeforeRecurrenceStart(): void
|
|
{
|
|
$slot = $this->createRecurringSlot(
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
recurrenceStart: new DateTimeImmutable('2026-09-01'),
|
|
recurrenceEnd: new DateTimeImmutable('2027-07-04'),
|
|
);
|
|
|
|
// 2026-08-25 is a Monday but before start
|
|
self::assertFalse($slot->isActiveOnDate(new DateTimeImmutable('2026-08-25')));
|
|
}
|
|
|
|
#[Test]
|
|
public function isActiveOnDateReturnsFalseAfterRecurrenceEnd(): void
|
|
{
|
|
$slot = $this->createRecurringSlot(
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
recurrenceStart: new DateTimeImmutable('2026-09-01'),
|
|
recurrenceEnd: new DateTimeImmutable('2027-07-04'),
|
|
);
|
|
|
|
// 2027-07-07 is a Monday but after end
|
|
self::assertFalse($slot->isActiveOnDate(new DateTimeImmutable('2027-07-07')));
|
|
}
|
|
|
|
#[Test]
|
|
public function isActiveOnDateReturnsTrueWithNoBounds(): void
|
|
{
|
|
$slot = $this->createRecurringSlot(
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
recurrenceStart: null,
|
|
recurrenceEnd: null,
|
|
);
|
|
|
|
// Any Monday should work
|
|
self::assertTrue($slot->isActiveOnDate(new DateTimeImmutable('2026-09-07')));
|
|
}
|
|
|
|
#[Test]
|
|
public function isActiveOnDateReturnsFalseForNonRecurringSlot(): void
|
|
{
|
|
$slot = ScheduleSlot::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
timeSlot: new TimeSlot('08:00', '09:00'),
|
|
room: null,
|
|
isRecurring: false,
|
|
now: new DateTimeImmutable('2026-09-01 10:00:00'),
|
|
);
|
|
|
|
self::assertFalse($slot->isActiveOnDate(new DateTimeImmutable('2026-09-07')));
|
|
}
|
|
|
|
#[Test]
|
|
public function isActiveOnDateIncludesBoundaryDates(): void
|
|
{
|
|
$slot = $this->createRecurringSlot(
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
recurrenceStart: new DateTimeImmutable('2026-09-07'),
|
|
recurrenceEnd: new DateTimeImmutable('2026-09-07'),
|
|
);
|
|
|
|
// Exact start = exact end date
|
|
self::assertTrue($slot->isActiveOnDate(new DateTimeImmutable('2026-09-07')));
|
|
}
|
|
|
|
#[Test]
|
|
public function reconstituteRestoresRecurrenceBounds(): void
|
|
{
|
|
$recurrenceStart = new DateTimeImmutable('2026-09-01');
|
|
$recurrenceEnd = new DateTimeImmutable('2027-07-04');
|
|
|
|
$slot = ScheduleSlot::reconstitute(
|
|
id: ScheduleSlotId::generate(),
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
dayOfWeek: DayOfWeek::FRIDAY,
|
|
timeSlot: new TimeSlot('14:00', '15:30'),
|
|
room: null,
|
|
isRecurring: true,
|
|
createdAt: new DateTimeImmutable('2026-03-01 10:00:00'),
|
|
updatedAt: new DateTimeImmutable('2026-03-02 14:00:00'),
|
|
recurrenceStart: $recurrenceStart,
|
|
recurrenceEnd: $recurrenceEnd,
|
|
);
|
|
|
|
self::assertEquals($recurrenceStart, $slot->recurrenceStart);
|
|
self::assertEquals($recurrenceEnd, $slot->recurrenceEnd);
|
|
}
|
|
|
|
private function createSlot(?string $room = null): ScheduleSlot
|
|
{
|
|
return ScheduleSlot::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
dayOfWeek: DayOfWeek::MONDAY,
|
|
timeSlot: new TimeSlot('08:00', '09:00'),
|
|
room: $room,
|
|
isRecurring: true,
|
|
now: new DateTimeImmutable('2026-03-01 10:00:00'),
|
|
);
|
|
}
|
|
|
|
private function createRecurringSlot(
|
|
DayOfWeek $dayOfWeek,
|
|
?DateTimeImmutable $recurrenceStart,
|
|
?DateTimeImmutable $recurrenceEnd,
|
|
): ScheduleSlot {
|
|
return ScheduleSlot::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString(self::CLASS_ID),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
dayOfWeek: $dayOfWeek,
|
|
timeSlot: new TimeSlot('08:00', '09:00'),
|
|
room: null,
|
|
isRecurring: true,
|
|
now: new DateTimeImmutable('2026-09-01 10:00:00'),
|
|
recurrenceStart: $recurrenceStart,
|
|
recurrenceEnd: $recurrenceEnd,
|
|
);
|
|
}
|
|
}
|