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 CreateScheduleExceptionHandler( $this->slotRepository, $this->exceptionRepository, $clock, ); } #[Test] public function cancelOccurrenceCreatesACancelledException(): void { $slot = $this->createAndSaveSlot(); $command = new CreateScheduleExceptionCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, exceptionDate: '2026-10-05', type: 'cancelled', reason: 'Grève', createdBy: self::CREATOR_ID, ); $exception = ($this->handler)($command); self::assertSame(ScheduleExceptionType::CANCELLED, $exception->type); self::assertSame('2026-10-05', $exception->exceptionDate->format('Y-m-d')); self::assertSame('Grève', $exception->reason); self::assertNull($exception->newTimeSlot); } #[Test] public function modifyOccurrenceCreatesAModifiedException(): void { $slot = $this->createAndSaveSlot(); $newTeacherId = '550e8400-e29b-41d4-a716-446655440011'; $command = new CreateScheduleExceptionCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, exceptionDate: '2026-10-05', type: 'modified', newStartTime: '10:00', newEndTime: '11:00', newRoom: 'Salle 301', newTeacherId: $newTeacherId, reason: 'Changement de salle', createdBy: self::CREATOR_ID, ); $exception = ($this->handler)($command); 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(UserId::fromString($newTeacherId))); } #[Test] public function exceptionIsSavedToRepository(): void { $slot = $this->createAndSaveSlot(); $command = new CreateScheduleExceptionCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, exceptionDate: '2026-10-05', type: 'cancelled', createdBy: self::CREATOR_ID, ); $exception = ($this->handler)($command); $tenantId = TenantId::fromString(self::TENANT_ID); $found = $this->exceptionRepository->findById($exception->id, $tenantId); self::assertNotNull($found); } #[Test] public function throwsWhenSlotNotFound(): void { $this->expectException(\App\Scolarite\Domain\Exception\ScheduleSlotNotFoundException::class); $command = new CreateScheduleExceptionCommand( tenantId: self::TENANT_ID, slotId: '550e8400-e29b-41d4-a716-446655440099', exceptionDate: '2026-10-05', type: 'cancelled', createdBy: self::CREATOR_ID, ); ($this->handler)($command); } #[Test] public function throwsWhenDateIsWrongDayOfWeek(): void { $slot = $this->createAndSaveSlot(); $this->expectException(DateExceptionInvalideException::class); // Slot is MONDAY, but 2026-10-06 is a Tuesday $command = new CreateScheduleExceptionCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, exceptionDate: '2026-10-06', type: 'cancelled', createdBy: self::CREATOR_ID, ); ($this->handler)($command); } #[Test] public function throwsWhenDateIsOutsideRecurrenceBounds(): 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: true, now: new DateTimeImmutable('2026-09-01 10:00:00'), recurrenceStart: new DateTimeImmutable('2026-09-07'), recurrenceEnd: new DateTimeImmutable('2026-09-28'), ); $this->slotRepository->save($slot); $this->expectException(DateExceptionInvalideException::class); // 2026-10-05 is a Monday but after recurrenceEnd (2026-09-28) $command = new CreateScheduleExceptionCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, exceptionDate: '2026-10-05', type: 'cancelled', createdBy: self::CREATOR_ID, ); ($this->handler)($command); } private function createAndSaveSlot(): 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'), ); $this->slotRepository->save($slot); return $slot; } }