feat: Permettre la définition d'une semaine type récurrente pour l'emploi du temps
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).
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Domain\Model\Schedule;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleException;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleExceptionId;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleExceptionType;
|
||||
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 ScheduleExceptionTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string SLOT_ID = '550e8400-e29b-41d4-a716-446655440050';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
private const string CREATOR_ID = '550e8400-e29b-41d4-a716-446655440099';
|
||||
|
||||
#[Test]
|
||||
public function annulerCreatesACancelledExceptionWithCorrectProperties(): void
|
||||
{
|
||||
$exception = ScheduleException::annuler(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
slotId: ScheduleSlotId::fromString(self::SLOT_ID),
|
||||
exceptionDate: new DateTimeImmutable('2026-10-05'),
|
||||
reason: 'Grève',
|
||||
createdBy: UserId::fromString(self::CREATOR_ID),
|
||||
now: new DateTimeImmutable('2026-10-01 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertTrue($exception->tenantId->equals(TenantId::fromString(self::TENANT_ID)));
|
||||
self::assertTrue($exception->slotId->equals(ScheduleSlotId::fromString(self::SLOT_ID)));
|
||||
self::assertSame('2026-10-05', $exception->exceptionDate->format('Y-m-d'));
|
||||
self::assertSame(ScheduleExceptionType::CANCELLED, $exception->type);
|
||||
self::assertSame('Grève', $exception->reason);
|
||||
self::assertNull($exception->newTimeSlot);
|
||||
self::assertNull($exception->newRoom);
|
||||
self::assertNull($exception->newTeacherId);
|
||||
self::assertTrue($exception->createdBy->equals(UserId::fromString(self::CREATOR_ID)));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierCreatesAModifiedExceptionWithNewValues(): void
|
||||
{
|
||||
$newTimeSlot = new TimeSlot('10:00', '11:00');
|
||||
$newTeacherId = UserId::fromString(self::TEACHER_ID);
|
||||
|
||||
$exception = ScheduleException::modifier(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
slotId: ScheduleSlotId::fromString(self::SLOT_ID),
|
||||
exceptionDate: new DateTimeImmutable('2026-10-05'),
|
||||
newTimeSlot: $newTimeSlot,
|
||||
newRoom: 'Salle 301',
|
||||
newTeacherId: $newTeacherId,
|
||||
reason: 'Changement de salle',
|
||||
createdBy: UserId::fromString(self::CREATOR_ID),
|
||||
now: new DateTimeImmutable('2026-10-01 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertSame(ScheduleExceptionType::MODIFIED, $exception->type);
|
||||
self::assertNotNull($exception->newTimeSlot);
|
||||
self::assertSame('10:00', $exception->newTimeSlot->startTime);
|
||||
self::assertSame('11:00', $exception->newTimeSlot->endTime);
|
||||
self::assertSame('Salle 301', $exception->newRoom);
|
||||
self::assertTrue($exception->newTeacherId->equals($newTeacherId));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierWithPartialOverridesKeepsNullsForUnchangedFields(): void
|
||||
{
|
||||
$exception = ScheduleException::modifier(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
slotId: ScheduleSlotId::fromString(self::SLOT_ID),
|
||||
exceptionDate: new DateTimeImmutable('2026-10-05'),
|
||||
newTimeSlot: null,
|
||||
newRoom: 'Salle 302',
|
||||
newTeacherId: null,
|
||||
reason: null,
|
||||
createdBy: UserId::fromString(self::CREATOR_ID),
|
||||
now: new DateTimeImmutable('2026-10-01 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertSame(ScheduleExceptionType::MODIFIED, $exception->type);
|
||||
self::assertNull($exception->newTimeSlot);
|
||||
self::assertSame('Salle 302', $exception->newRoom);
|
||||
self::assertNull($exception->newTeacherId);
|
||||
self::assertNull($exception->reason);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isCancelledReturnsTrueForCancelledType(): void
|
||||
{
|
||||
$exception = ScheduleException::annuler(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
slotId: ScheduleSlotId::fromString(self::SLOT_ID),
|
||||
exceptionDate: new DateTimeImmutable('2026-10-05'),
|
||||
reason: null,
|
||||
createdBy: UserId::fromString(self::CREATOR_ID),
|
||||
now: new DateTimeImmutable('2026-10-01 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertTrue($exception->isCancelled());
|
||||
self::assertFalse($exception->isModified());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function isModifiedReturnsTrueForModifiedType(): void
|
||||
{
|
||||
$exception = ScheduleException::modifier(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
slotId: ScheduleSlotId::fromString(self::SLOT_ID),
|
||||
exceptionDate: new DateTimeImmutable('2026-10-05'),
|
||||
newTimeSlot: new TimeSlot('14:00', '15:00'),
|
||||
newRoom: null,
|
||||
newTeacherId: null,
|
||||
reason: null,
|
||||
createdBy: UserId::fromString(self::CREATOR_ID),
|
||||
now: new DateTimeImmutable('2026-10-01 10:00:00'),
|
||||
);
|
||||
|
||||
self::assertTrue($exception->isModified());
|
||||
self::assertFalse($exception->isCancelled());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reconstituteRestoresAllPropertiesWithoutEvents(): void
|
||||
{
|
||||
$id = ScheduleExceptionId::generate();
|
||||
$tenantId = TenantId::fromString(self::TENANT_ID);
|
||||
$slotId = ScheduleSlotId::fromString(self::SLOT_ID);
|
||||
$newTimeSlot = new TimeSlot('11:00', '12:00');
|
||||
$newTeacherId = UserId::fromString(self::TEACHER_ID);
|
||||
$createdBy = UserId::fromString(self::CREATOR_ID);
|
||||
$createdAt = new DateTimeImmutable('2026-10-01 10:00:00');
|
||||
|
||||
$exception = ScheduleException::reconstitute(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
slotId: $slotId,
|
||||
exceptionDate: new DateTimeImmutable('2026-10-05'),
|
||||
type: ScheduleExceptionType::MODIFIED,
|
||||
newTimeSlot: $newTimeSlot,
|
||||
newRoom: 'Salle 303',
|
||||
newTeacherId: $newTeacherId,
|
||||
reason: 'Interversion',
|
||||
createdBy: $createdBy,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
self::assertTrue($exception->id->equals($id));
|
||||
self::assertTrue($exception->tenantId->equals($tenantId));
|
||||
self::assertTrue($exception->slotId->equals($slotId));
|
||||
self::assertSame('2026-10-05', $exception->exceptionDate->format('Y-m-d'));
|
||||
self::assertSame(ScheduleExceptionType::MODIFIED, $exception->type);
|
||||
self::assertSame('11:00', $exception->newTimeSlot->startTime);
|
||||
self::assertSame('Salle 303', $exception->newRoom);
|
||||
self::assertTrue($exception->newTeacherId->equals($newTeacherId));
|
||||
self::assertSame('Interversion', $exception->reason);
|
||||
self::assertTrue($exception->createdBy->equals($createdBy));
|
||||
self::assertEquals($createdAt, $exception->createdAt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user