Lors d'un drag & drop d'un créneau vers un autre jour de la semaine, le nouveau créneau devenait invisible car recurrenceStart était fixé à la date d'occurrence (le jour source). Si le jour cible tombait avant cette date dans la semaine, isActiveOnDate retournait false. recurrenceStart est maintenant fixé au lundi de la semaine d'occurrence, ce qui garantit la visibilité du créneau dès la semaine du déplacement.
210 lines
7.9 KiB
PHP
210 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Application\Command\UpdateRecurringSlot;
|
|
|
|
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
|
use App\Administration\Domain\Model\Subject\SubjectId;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Scolarite\Application\Command\UpdateRecurringSlot\UpdateRecurringSlotCommand;
|
|
use App\Scolarite\Application\Command\UpdateRecurringSlot\UpdateRecurringSlotHandler;
|
|
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
|
|
use App\Scolarite\Domain\Model\Schedule\ScheduleExceptionType;
|
|
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
|
|
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleExceptionRepository;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleSlotRepository;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class UpdateRecurringSlotHandlerTest 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';
|
|
private const string NEW_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440011';
|
|
private const string UPDATER_ID = '550e8400-e29b-41d4-a716-446655440099';
|
|
|
|
private InMemoryScheduleSlotRepository $slotRepository;
|
|
private InMemoryScheduleExceptionRepository $exceptionRepository;
|
|
private UpdateRecurringSlotHandler $handler;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->slotRepository = new InMemoryScheduleSlotRepository();
|
|
$this->exceptionRepository = new InMemoryScheduleExceptionRepository();
|
|
|
|
$clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-10-01 10:00:00');
|
|
}
|
|
};
|
|
|
|
$this->handler = new UpdateRecurringSlotHandler(
|
|
$this->slotRepository,
|
|
$this->exceptionRepository,
|
|
$clock,
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function thisOccurrenceCreatesAnException(): void
|
|
{
|
|
$slot = $this->createAndSaveSlot();
|
|
|
|
$command = new UpdateRecurringSlotCommand(
|
|
tenantId: self::TENANT_ID,
|
|
slotId: (string) $slot->id,
|
|
occurrenceDate: '2026-10-05',
|
|
scope: 'this_occurrence',
|
|
classId: self::CLASS_ID,
|
|
subjectId: self::SUBJECT_ID,
|
|
teacherId: self::NEW_TEACHER_ID,
|
|
dayOfWeek: DayOfWeek::MONDAY->value,
|
|
startTime: '10:00',
|
|
endTime: '11:00',
|
|
room: 'Salle 301',
|
|
updatedBy: self::UPDATER_ID,
|
|
);
|
|
|
|
$result = ($this->handler)($command);
|
|
|
|
self::assertNotNull($result['exception']);
|
|
self::assertSame(ScheduleExceptionType::MODIFIED, $result['exception']->type);
|
|
self::assertSame('10:00', $result['exception']->newTimeSlot->startTime);
|
|
self::assertSame('Salle 301', $result['exception']->newRoom);
|
|
}
|
|
|
|
#[Test]
|
|
public function allFutureEndCurrentRecurrenceAndCreatesNewSlot(): void
|
|
{
|
|
$slot = $this->createAndSaveSlot(
|
|
recurrenceStart: new DateTimeImmutable('2026-09-01'),
|
|
recurrenceEnd: new DateTimeImmutable('2027-07-04'),
|
|
);
|
|
|
|
$command = new UpdateRecurringSlotCommand(
|
|
tenantId: self::TENANT_ID,
|
|
slotId: (string) $slot->id,
|
|
occurrenceDate: '2026-10-12',
|
|
scope: 'all_future',
|
|
classId: self::CLASS_ID,
|
|
subjectId: self::SUBJECT_ID,
|
|
teacherId: self::NEW_TEACHER_ID,
|
|
dayOfWeek: DayOfWeek::MONDAY->value,
|
|
startTime: '10:00',
|
|
endTime: '11:00',
|
|
room: 'Salle 301',
|
|
updatedBy: self::UPDATER_ID,
|
|
);
|
|
|
|
$result = ($this->handler)($command);
|
|
|
|
// Original slot's recurrence ends before the change date
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
$updatedOriginal = $this->slotRepository->get($slot->id, $tenantId);
|
|
self::assertSame('2026-10-11', $updatedOriginal->recurrenceEnd->format('Y-m-d'));
|
|
|
|
// New slot starts from the change date
|
|
self::assertNotNull($result['newSlot']);
|
|
self::assertSame('2026-10-12', $result['newSlot']->recurrenceStart->format('Y-m-d'));
|
|
self::assertSame('2027-07-04', $result['newSlot']->recurrenceEnd->format('Y-m-d'));
|
|
self::assertSame('10:00', $result['newSlot']->timeSlot->startTime);
|
|
self::assertTrue($result['newSlot']->teacherId->equals(UserId::fromString(self::NEW_TEACHER_ID)));
|
|
}
|
|
|
|
#[Test]
|
|
public function allFutureCrossDayMoveSetsRecurrenceStartToWeekMonday(): void
|
|
{
|
|
// Wednesday slot — moving it to Monday
|
|
$slot = $this->createAndSaveSlot(
|
|
dayOfWeek: DayOfWeek::WEDNESDAY,
|
|
recurrenceStart: new DateTimeImmutable('2026-09-01'),
|
|
recurrenceEnd: new DateTimeImmutable('2027-07-04'),
|
|
);
|
|
|
|
$command = new UpdateRecurringSlotCommand(
|
|
tenantId: self::TENANT_ID,
|
|
slotId: (string) $slot->id,
|
|
occurrenceDate: '2026-10-14', // Wednesday
|
|
scope: 'all_future',
|
|
classId: self::CLASS_ID,
|
|
subjectId: self::SUBJECT_ID,
|
|
teacherId: self::TEACHER_ID,
|
|
dayOfWeek: DayOfWeek::MONDAY->value,
|
|
startTime: '08:00',
|
|
endTime: '09:00',
|
|
room: null,
|
|
updatedBy: self::UPDATER_ID,
|
|
);
|
|
|
|
$result = ($this->handler)($command);
|
|
|
|
$newSlot = $result['newSlot'];
|
|
self::assertNotNull($newSlot);
|
|
self::assertSame(DayOfWeek::MONDAY, $newSlot->dayOfWeek);
|
|
// recurrenceStart must be the Monday of the occurrence week (2026-10-12),
|
|
// not the occurrence date (2026-10-14), so the slot is visible this week
|
|
self::assertSame('2026-10-12', $newSlot->recurrenceStart->format('Y-m-d'));
|
|
}
|
|
|
|
#[Test]
|
|
public function allFutureWithNoOriginalEndUsesNullForNewSlotEnd(): void
|
|
{
|
|
$slot = $this->createAndSaveSlot(
|
|
recurrenceStart: new DateTimeImmutable('2026-09-01'),
|
|
recurrenceEnd: null,
|
|
);
|
|
|
|
$command = new UpdateRecurringSlotCommand(
|
|
tenantId: self::TENANT_ID,
|
|
slotId: (string) $slot->id,
|
|
occurrenceDate: '2026-10-12',
|
|
scope: 'all_future',
|
|
classId: self::CLASS_ID,
|
|
subjectId: self::SUBJECT_ID,
|
|
teacherId: self::TEACHER_ID,
|
|
dayOfWeek: DayOfWeek::MONDAY->value,
|
|
startTime: '10:00',
|
|
endTime: '11:00',
|
|
room: null,
|
|
updatedBy: self::UPDATER_ID,
|
|
);
|
|
|
|
$result = ($this->handler)($command);
|
|
|
|
self::assertNotNull($result['newSlot']);
|
|
self::assertNull($result['newSlot']->recurrenceEnd);
|
|
}
|
|
|
|
private function createAndSaveSlot(
|
|
DayOfWeek $dayOfWeek = DayOfWeek::MONDAY,
|
|
?DateTimeImmutable $recurrenceStart = null,
|
|
?DateTimeImmutable $recurrenceEnd = null,
|
|
): ScheduleSlot {
|
|
$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,
|
|
timeSlot: new TimeSlot('08:00', '09:00'),
|
|
room: null,
|
|
isRecurring: true,
|
|
now: new DateTimeImmutable('2026-09-01 10:00:00'),
|
|
recurrenceStart: $recurrenceStart,
|
|
recurrenceEnd: $recurrenceEnd,
|
|
);
|
|
|
|
$this->slotRepository->save($slot);
|
|
|
|
return $slot;
|
|
}
|
|
}
|