repository = new InMemoryScheduleSlotRepository(); $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-02 10:00:00'); } }; $this->handler = new CreateScheduleSlotHandler( $this->repository, new ScheduleConflictDetector($this->repository), $this->createAlwaysAssignedChecker(), $clock, ); } #[Test] public function createsSlotSuccessfully(): void { $result = ($this->handler)($this->createCommand()); /** @var ScheduleSlot $slot */ $slot = $result['slot']; self::assertTrue($slot->classId->equals(ClassId::fromString(self::CLASS_ID))); self::assertSame(DayOfWeek::MONDAY, $slot->dayOfWeek); self::assertSame('08:00', $slot->timeSlot->startTime); self::assertSame('09:00', $slot->timeSlot->endTime); self::assertEmpty($result['conflicts']); } #[Test] public function savesSlotToRepository(): void { $result = ($this->handler)($this->createCommand()); /** @var ScheduleSlot $slot */ $slot = $result['slot']; $found = $this->repository->findById($slot->id, TenantId::fromString(self::TENANT_ID)); self::assertNotNull($found); self::assertTrue($found->id->equals($slot->id)); } #[Test] public function detectsConflictsWithoutSaving(): void { // Créer un slot existant ($this->handler)($this->createCommand()); // Même enseignant, même créneau, classe différente $result = ($this->handler)(new CreateScheduleSlotCommand( tenantId: self::TENANT_ID, classId: '550e8400-e29b-41d4-a716-446655440021', subjectId: self::SUBJECT_ID, teacherId: self::TEACHER_ID, dayOfWeek: DayOfWeek::MONDAY->value, startTime: '08:30', endTime: '09:30', room: null, )); self::assertNotEmpty($result['conflicts']); // Le slot conflictuel ne devrait PAS être sauvegardé $allSlots = $this->repository->findByTeacher( UserId::fromString(self::TEACHER_ID), TenantId::fromString(self::TENANT_ID), ); self::assertCount(1, $allSlots); } #[Test] public function forceSavesSlotWithConflicts(): void { ($this->handler)($this->createCommand()); $result = ($this->handler)(new CreateScheduleSlotCommand( tenantId: self::TENANT_ID, classId: '550e8400-e29b-41d4-a716-446655440021', subjectId: self::SUBJECT_ID, teacherId: self::TEACHER_ID, dayOfWeek: DayOfWeek::MONDAY->value, startTime: '08:30', endTime: '09:30', room: null, forceConflicts: true, )); self::assertNotEmpty($result['conflicts']); // Malgré les conflits, le slot est sauvegardé $allSlots = $this->repository->findByTeacher( UserId::fromString(self::TEACHER_ID), TenantId::fromString(self::TENANT_ID), ); self::assertCount(2, $allSlots); } #[Test] public function rejectsUnassignedTeacher(): void { $clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-02 10:00:00'); } }; $handler = new CreateScheduleSlotHandler( $this->repository, new ScheduleConflictDetector($this->repository), $this->createNeverAssignedChecker(), $clock, ); $this->expectException(EnseignantNonAffecteException::class); ($handler)($this->createCommand()); } private function createCommand(): CreateScheduleSlotCommand { return new CreateScheduleSlotCommand( tenantId: self::TENANT_ID, classId: self::CLASS_ID, subjectId: self::SUBJECT_ID, teacherId: self::TEACHER_ID, dayOfWeek: DayOfWeek::MONDAY->value, startTime: '08:00', endTime: '09:00', room: null, ); } private function createAlwaysAssignedChecker(): EnseignantAffectationChecker { return new class implements EnseignantAffectationChecker { public function estAffecte( UserId $teacherId, ClassId $classId, SubjectId $subjectId, TenantId $tenantId, ): bool { return true; } }; } private function createNeverAssignedChecker(): EnseignantAffectationChecker { return new class implements EnseignantAffectationChecker { public function estAffecte( UserId $teacherId, ClassId $classId, SubjectId $subjectId, TenantId $tenantId, ): bool { return false; } }; } }