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 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( ?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::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, ); $this->slotRepository->save($slot); return $slot; } }