repository = new InMemoryScheduleSlotRepository(); $this->clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-02 15:00:00'); } }; $this->handler = new UpdateScheduleSlotHandler( $this->repository, new ScheduleConflictDetector($this->repository), $this->createAlwaysAssignedChecker(), $this->clock, ); } #[Test] public function updatesSlotProperties(): void { $slot = $this->createAndSaveSlot(); $newSubjectId = '550e8400-e29b-41d4-a716-446655440031'; $result = ($this->handler)(new UpdateScheduleSlotCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, classId: self::CLASS_ID, subjectId: $newSubjectId, teacherId: self::TEACHER_ID, dayOfWeek: DayOfWeek::WEDNESDAY->value, startTime: '10:00', endTime: '11:00', room: 'Salle 301', )); /** @var ScheduleSlot $updated */ $updated = $result['slot']; self::assertTrue($updated->subjectId->equals(SubjectId::fromString($newSubjectId))); self::assertSame(DayOfWeek::WEDNESDAY, $updated->dayOfWeek); self::assertSame('10:00', $updated->timeSlot->startTime); self::assertSame('Salle 301', $updated->room); self::assertEmpty($result['conflicts']); } #[Test] public function updatesClassId(): void { $slot = $this->createAndSaveSlot(); $result = ($this->handler)(new UpdateScheduleSlotCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, classId: self::OTHER_CLASS_ID, subjectId: self::SUBJECT_ID, teacherId: self::TEACHER_ID, dayOfWeek: DayOfWeek::MONDAY->value, startTime: '08:00', endTime: '09:00', room: null, )); /** @var ScheduleSlot $updated */ $updated = $result['slot']; self::assertTrue($updated->classId->equals(ClassId::fromString(self::OTHER_CLASS_ID))); self::assertFalse($updated->classId->equals(ClassId::fromString(self::CLASS_ID))); } #[Test] public function persistsClassIdChange(): void { $slot = $this->createAndSaveSlot(); ($this->handler)(new UpdateScheduleSlotCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, classId: self::OTHER_CLASS_ID, subjectId: self::SUBJECT_ID, teacherId: self::TEACHER_ID, dayOfWeek: DayOfWeek::MONDAY->value, startTime: '08:00', endTime: '09:00', room: null, )); $persisted = $this->repository->get($slot->id, TenantId::fromString(self::TENANT_ID)); self::assertTrue($persisted->classId->equals(ClassId::fromString(self::OTHER_CLASS_ID))); } #[Test] public function excludesSelfFromConflictDetection(): void { $slot = $this->createAndSaveSlot(); // Modifier le slot sans changer le créneau → pas de conflit avec lui-même $result = ($this->handler)(new UpdateScheduleSlotCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->id, classId: self::CLASS_ID, subjectId: self::SUBJECT_ID, teacherId: self::TEACHER_ID, dayOfWeek: DayOfWeek::MONDAY->value, startTime: '08:00', endTime: '09:00', room: 'Salle 101', )); self::assertEmpty($result['conflicts']); } #[Test] public function rejectsUnassignedTeacher(): void { $slot = $this->createAndSaveSlot(); $handler = new UpdateScheduleSlotHandler( $this->repository, new ScheduleConflictDetector($this->repository), $this->createNeverAssignedChecker(), $this->clock, ); $this->expectException(EnseignantNonAffecteException::class); ($handler)(new UpdateScheduleSlotCommand( tenantId: self::TENANT_ID, slotId: (string) $slot->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 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-03-01 10:00:00'), ); $this->repository->save($slot); return $slot; } 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; } }; } }