replacementRepository = new InMemoryTeacherReplacementRepository(); $this->userRepository = new InMemoryUserRepository(); $this->clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-15 10:00:00'); } }; $this->seedTestData(); } #[Test] public function itCreatesReplacementSuccessfully(): void { $handler = $this->createHandler(); $command = $this->createCommand(); $replacement = $handler($command); self::assertNotEmpty((string) $replacement->id); self::assertSame(ReplacementStatus::ACTIVE, $replacement->status); self::assertNull($replacement->endedAt); } #[Test] public function itPersistsReplacementInRepository(): void { $handler = $this->createHandler(); $command = $this->createCommand(); $created = $handler($command); $replacement = $this->replacementRepository->get( TeacherReplacementId::fromString((string) $created->id), TenantId::fromString(self::TENANT_ID), ); self::assertSame(ReplacementStatus::ACTIVE, $replacement->status); self::assertCount(1, $replacement->classes); } #[Test] public function itSetsAllPropertiesCorrectly(): void { $handler = $this->createHandler(); $command = $this->createCommand(); $replacement = $handler($command); self::assertTrue($replacement->replacedTeacherId->equals(UserId::fromString(self::REPLACED_TEACHER_ID))); self::assertTrue($replacement->replacementTeacherId->equals(UserId::fromString(self::REPLACEMENT_TEACHER_ID))); self::assertEquals(new DateTimeImmutable('2026-03-01'), $replacement->startDate); self::assertEquals(new DateTimeImmutable('2026-03-31'), $replacement->endDate); self::assertSame('Congé maladie', $replacement->reason); } #[Test] public function itThrowsWhenReplacedTeacherDoesNotExist(): void { $handler = $this->createHandler(); $this->expectException(UserNotFoundException::class); $handler($this->createCommand(replacedTeacherId: '550e8400-e29b-41d4-a716-446655440088')); } #[Test] public function itThrowsWhenReplacementTeacherDoesNotExist(): void { $handler = $this->createHandler(); $this->expectException(UserNotFoundException::class); $handler($this->createCommand(replacementTeacherId: '550e8400-e29b-41d4-a716-446655440088')); } #[Test] public function itThrowsWhenSameTeacher(): void { $handler = $this->createHandler(); $this->expectException(RemplacementSameTeacherException::class); $handler($this->createCommand(replacementTeacherId: self::REPLACED_TEACHER_ID)); } #[Test] public function itThrowsWhenEndDateBeforeStartDate(): void { $handler = $this->createHandler(); $this->expectException(DatesRemplacementInvalidesException::class); $handler($this->createCommand(startDate: '2026-03-31', endDate: '2026-03-01')); } #[Test] public function itThrowsWhenReplacedTeacherBelongsToDifferentTenant(): void { $handler = $this->createHandler(); $this->expectException(TenantMismatchException::class); $handler($this->createCommand(replacedTeacherId: self::OTHER_TENANT_TEACHER_ID)); } #[Test] public function itThrowsWhenReplacementTeacherBelongsToDifferentTenant(): void { $handler = $this->createHandler(); $this->expectException(TenantMismatchException::class); $handler($this->createCommand(replacementTeacherId: self::OTHER_TENANT_TEACHER_ID)); } #[Test] public function itThrowsWhenReplacedTeacherIsNotATeacher(): void { $handler = $this->createHandler(); $this->expectException(UtilisateurNonEnseignantException::class); $handler($this->createCommand(replacedTeacherId: self::ADMIN_USER_ID)); } #[Test] public function itThrowsWhenReplacementTeacherIsNotATeacher(): void { $handler = $this->createHandler(); $this->expectException(UtilisateurNonEnseignantException::class); $handler($this->createCommand(replacementTeacherId: self::ADMIN_USER_ID)); } private function seedTestData(): void { $tenantId = TenantId::fromString(self::TENANT_ID); $replacedTeacher = User::reconstitute( id: UserId::fromString(self::REPLACED_TEACHER_ID), email: new Email('replaced@example.com'), roles: [Role::PROF], tenantId: $tenantId, schoolName: 'École Test', statut: StatutCompte::EN_ATTENTE, dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-15'), hashedPassword: null, activatedAt: null, consentementParental: null, ); $this->userRepository->save($replacedTeacher); $replacementTeacher = User::reconstitute( id: UserId::fromString(self::REPLACEMENT_TEACHER_ID), email: new Email('replacement@example.com'), roles: [Role::PROF], tenantId: $tenantId, schoolName: 'École Test', statut: StatutCompte::EN_ATTENTE, dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-15'), hashedPassword: null, activatedAt: null, consentementParental: null, ); $this->userRepository->save($replacementTeacher); // Enseignant d'un autre tenant $otherTenantTeacher = User::reconstitute( id: UserId::fromString(self::OTHER_TENANT_TEACHER_ID), email: new Email('other-tenant@example.com'), roles: [Role::PROF], tenantId: TenantId::fromString(self::OTHER_TENANT_ID), schoolName: 'Autre École', statut: StatutCompte::EN_ATTENTE, dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-15'), hashedPassword: null, activatedAt: null, consentementParental: null, ); $this->userRepository->save($otherTenantTeacher); // Utilisateur admin (pas enseignant) dans le même tenant $adminUser = User::reconstitute( id: UserId::fromString(self::ADMIN_USER_ID), email: new Email('admin@example.com'), roles: [Role::ADMIN], tenantId: $tenantId, schoolName: 'École Test', statut: StatutCompte::EN_ATTENTE, dateNaissance: null, createdAt: new DateTimeImmutable('2026-01-15'), hashedPassword: null, activatedAt: null, consentementParental: null, ); $this->userRepository->save($adminUser); } private function createHandler(): DesignateReplacementHandler { return new DesignateReplacementHandler( $this->replacementRepository, $this->userRepository, $this->clock, ); } private function createCommand( ?string $replacedTeacherId = null, ?string $replacementTeacherId = null, ?string $startDate = null, ?string $endDate = null, ): DesignateReplacementCommand { return new DesignateReplacementCommand( tenantId: self::TENANT_ID, replacedTeacherId: $replacedTeacherId ?? self::REPLACED_TEACHER_ID, replacementTeacherId: $replacementTeacherId ?? self::REPLACEMENT_TEACHER_ID, startDate: $startDate ?? '2026-03-01', endDate: $endDate ?? '2026-03-31', classes: [ ['classId' => self::CLASS_ID, 'subjectId' => self::SUBJECT_ID], ], reason: 'Congé maladie', createdBy: self::CREATED_BY_ID, ); } }