classAssignmentRepository = new InMemoryClassAssignmentRepository(); $this->classRepository = new InMemoryClassRepository(); $this->clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-02-21 10:00:00'); } }; $this->seedTestData(); } #[Test] public function itChangesStudentClass(): void { $handler = $this->createHandler(); $command = $this->createCommand(); $assignment = $handler($command); self::assertTrue($assignment->classId->equals(ClassId::fromString(self::NEW_CLASS_ID))); } #[Test] public function itThrowsWhenNewClassDoesNotExist(): void { $handler = $this->createHandler(); $command = $this->createCommand(newClassId: '550e8400-e29b-41d4-a716-446655440099'); $this->expectException(ClasseNotFoundException::class); $handler($command); } #[Test] public function itCreatesAssignmentWhenNoneExists(): void { $handler = $this->createHandler(); $newStudentId = '550e8400-e29b-41d4-a716-446655440070'; $command = $this->createCommand(studentId: $newStudentId); $assignment = $handler($command); self::assertTrue($assignment->studentId->equals(UserId::fromString($newStudentId))); self::assertTrue($assignment->classId->equals(ClassId::fromString(self::NEW_CLASS_ID))); self::assertTrue($assignment->academicYearId->equals(AcademicYearId::fromString(self::ACADEMIC_YEAR_ID))); } #[Test] public function itThrowsWhenClassBelongsToAnotherTenant(): void { $crossTenantClassId = '550e8400-e29b-41d4-a716-446655440030'; $this->classRepository->save(SchoolClass::reconstitute( id: ClassId::fromString($crossTenantClassId), tenantId: TenantId::fromString('550e8400-e29b-41d4-a716-446655440002'), schoolId: SchoolId::fromString(self::SCHOOL_ID), academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), name: new ClassName('5ème B'), level: SchoolLevel::CINQUIEME, capacity: 30, status: ClassStatus::ACTIVE, description: null, createdAt: new DateTimeImmutable('2026-01-15'), updatedAt: new DateTimeImmutable('2026-01-15'), deletedAt: null, )); $handler = $this->createHandler(); $command = $this->createCommand(newClassId: $crossTenantClassId); $this->expectException(ClasseNotFoundException::class); $handler($command); } #[Test] public function itThrowsWhenClassIsArchived(): void { $archivedClassId = '550e8400-e29b-41d4-a716-446655440031'; $this->classRepository->save(SchoolClass::reconstitute( id: ClassId::fromString($archivedClassId), tenantId: TenantId::fromString(self::TENANT_ID), schoolId: SchoolId::fromString(self::SCHOOL_ID), academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), name: new ClassName('6ème C'), level: SchoolLevel::SIXIEME, capacity: 30, status: ClassStatus::ARCHIVED, description: null, createdAt: new DateTimeImmutable('2026-01-15'), updatedAt: new DateTimeImmutable('2026-01-15'), deletedAt: null, )); $handler = $this->createHandler(); $command = $this->createCommand(newClassId: $archivedClassId); $this->expectException(ClasseNotFoundException::class); $handler($command); } private function seedTestData(): void { $oldClass = SchoolClass::reconstitute( id: ClassId::fromString(self::OLD_CLASS_ID), tenantId: TenantId::fromString(self::TENANT_ID), schoolId: SchoolId::fromString(self::SCHOOL_ID), academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), name: new ClassName('6ème A'), level: SchoolLevel::SIXIEME, capacity: 30, status: ClassStatus::ACTIVE, description: null, createdAt: new DateTimeImmutable('2026-01-15'), updatedAt: new DateTimeImmutable('2026-01-15'), deletedAt: null, ); $this->classRepository->save($oldClass); $newClass = SchoolClass::reconstitute( id: ClassId::fromString(self::NEW_CLASS_ID), tenantId: TenantId::fromString(self::TENANT_ID), schoolId: SchoolId::fromString(self::SCHOOL_ID), academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), name: new ClassName('6ème B'), level: SchoolLevel::SIXIEME, capacity: 30, status: ClassStatus::ACTIVE, description: null, createdAt: new DateTimeImmutable('2026-01-15'), updatedAt: new DateTimeImmutable('2026-01-15'), deletedAt: null, ); $this->classRepository->save($newClass); $assignment = ClassAssignment::affecter( tenantId: TenantId::fromString(self::TENANT_ID), studentId: UserId::fromString(self::STUDENT_ID), classId: ClassId::fromString(self::OLD_CLASS_ID), academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), assignedAt: new DateTimeImmutable('2026-01-20'), ); $this->classAssignmentRepository->save($assignment); } private function createHandler(): ChangeStudentClassHandler { return new ChangeStudentClassHandler( $this->classAssignmentRepository, $this->classRepository, $this->clock, ); } private function createCommand( ?string $newClassId = null, ?string $studentId = null, ): ChangeStudentClassCommand { return new ChangeStudentClassCommand( tenantId: self::TENANT_ID, studentId: $studentId ?? self::STUDENT_ID, newClassId: $newClassId ?? self::NEW_CLASS_ID, academicYearId: self::ACADEMIC_YEAR_ID, ); } }