seedFixtures(); } protected function tearDown(): void { /** @var Connection $connection */ $connection = static::getContainer()->get(Connection::class); $connection->executeStatement( 'DELETE FROM student_competency_results WHERE competency_evaluation_id IN (SELECT id FROM competency_evaluations WHERE evaluation_id IN (SELECT id FROM evaluations WHERE tenant_id = :tid))', ['tid' => self::TENANT_ID], ); $connection->executeStatement( 'DELETE FROM competency_evaluations WHERE evaluation_id IN (SELECT id FROM evaluations WHERE tenant_id = :tid)', ['tid' => self::TENANT_ID], ); $connection->executeStatement( 'DELETE FROM competencies WHERE framework_id IN (SELECT id FROM competency_frameworks WHERE tenant_id = :tid)', ['tid' => self::TENANT_ID], ); $connection->executeStatement( 'DELETE FROM competency_frameworks WHERE tenant_id = :tid', ['tid' => self::TENANT_ID], ); $connection->executeStatement( 'DELETE FROM evaluations WHERE tenant_id = :tid', ['tid' => self::TENANT_ID], ); parent::tearDown(); } // ========================================================================= // GET /competencies — Auth & Happy path // ========================================================================= #[Test] public function getCompetenciesReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', self::BASE_URL . '/competencies', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function getCompetenciesReturns200ForAuthenticatedUser(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/competencies', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(2, $data); self::assertSame(self::COMPETENCY1_ID, $data[0]['id']); self::assertSame('C1', $data[0]['code']); self::assertSame('Lire et comprendre', $data[0]['name']); self::assertSame(self::COMPETENCY2_ID, $data[1]['id']); self::assertSame('C2', $data[1]['code']); self::assertSame('Ecrire', $data[1]['name']); } // ========================================================================= // GET /competency-levels — Auth & Happy path // ========================================================================= #[Test] public function getCompetencyLevelsReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', self::BASE_URL . '/competency-levels', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function getCompetencyLevelsReturns200WithStandard4Levels(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/competency-levels', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(4, $data); self::assertSame('not_acquired', $data[0]['code']); self::assertSame('Non acquis', $data[0]['name']); self::assertSame('in_progress', $data[1]['code']); self::assertSame('acquired', $data[2]['code']); self::assertSame('exceeded', $data[3]['code']); } // ========================================================================= // GET /evaluations/{id}/competencies — Auth & edge cases // ========================================================================= #[Test] public function getEvaluationCompetenciesReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function getEvaluationCompetenciesReturns403ForNonOwner(): void { $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function getEvaluationCompetenciesReturns404ForUnknownEvaluation(): void { $unknownId = (string) EvaluationId::generate(); $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/evaluations/' . $unknownId . '/competencies', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(404); } #[Test] public function getEvaluationCompetenciesReturns200ForOwner(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(2, $data); self::assertSame((string) $this->evaluationId, $data[0]['evaluationId']); self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']); self::assertSame('C1', $data[0]['competencyCode']); self::assertSame(self::COMPETENCY2_ID, $data[1]['competencyId']); self::assertSame('C2', $data[1]['competencyCode']); } // ========================================================================= // GET /evaluations/{id}/competency-results — Auth & Happy path // ========================================================================= #[Test] public function getCompetencyResultsReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function getCompetencyResultsReturns403ForNonOwner(): void { $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function getCompetencyResultsReturns200ForOwnerWithGrid(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); // 2 students x 2 competencies = 4 rows in the result matrix self::assertCount(4, $data); // Find the seeded result (student1 + competency1 = acquired) $seededResult = null; foreach ($data as $row) { if ($row['studentId'] === self::STUDENT_ID && $row['competencyEvaluationId'] === $this->competencyEvaluation1Id) { $seededResult = $row; break; } } self::assertNotNull($seededResult); self::assertSame('acquired', $seededResult['levelCode']); } // ========================================================================= // PUT /evaluations/{id}/competency-results — Auth & Happy path // ========================================================================= #[Test] public function putCompetencyResultsReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [], ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function putCompetencyResultsReturns403ForNonOwner(): void { $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [], ], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function putCompetencyResultsReturns200AndSavesResults(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [ [ 'studentId' => self::STUDENT2_ID, 'competencyEvaluationId' => $this->competencyEvaluation1Id, 'levelCode' => 'in_progress', ], [ 'studentId' => self::STUDENT2_ID, 'competencyEvaluationId' => $this->competencyEvaluation2Id, 'levelCode' => 'not_acquired', ], ], ], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(2, $data); self::assertSame(self::STUDENT2_ID, $data[0]['studentId']); self::assertSame('in_progress', $data[0]['levelCode']); self::assertSame(self::STUDENT2_ID, $data[1]['studentId']); self::assertSame('not_acquired', $data[1]['levelCode']); } // ========================================================================= // POST /evaluations/{id}/competencies — Auth & Happy path // ========================================================================= #[Test] public function postLinkCompetenciesReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'competencyIds' => [self::COMPETENCY1_ID], ], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function postLinkCompetenciesReturns403ForNonOwner(): void { $client = $this->createAuthenticatedClient(self::OTHER_TEACHER_ID, ['ROLE_PROF']); $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'competencyIds' => [self::COMPETENCY1_ID], ], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function postLinkCompetenciesReturns200AndLinksCompetencies(): void { // First, remove existing links to test fresh linking /** @var Connection $connection */ $connection = static::getContainer()->get(Connection::class); $connection->executeStatement( 'DELETE FROM student_competency_results WHERE competency_evaluation_id IN (SELECT id FROM competency_evaluations WHERE evaluation_id = :eid)', ['eid' => (string) $this->evaluationId], ); $connection->executeStatement( 'DELETE FROM competency_evaluations WHERE evaluation_id = :eid', ['eid' => (string) $this->evaluationId], ); $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'competencyIds' => [self::COMPETENCY1_ID, self::COMPETENCY2_ID], ], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(2, $data); self::assertSame((string) $this->evaluationId, $data[0]['evaluationId']); self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']); self::assertSame(self::COMPETENCY2_ID, $data[1]['competencyId']); } // ========================================================================= // PUT /evaluations/{id}/competency-results — Validation (error paths) // ========================================================================= #[Test] public function putCompetencyResultsReturns400ForInvalidLevelCode(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [ [ 'studentId' => self::STUDENT_ID, 'competencyEvaluationId' => $this->competencyEvaluation1Id, 'levelCode' => 'INVALID_CODE', ], ], ], ]); self::assertResponseStatusCodeSame(400); } #[Test] public function putCompetencyResultsReturns400ForStudentNotInClass(): void { $outsiderId = 'cc999999-9999-9999-9999-999999999999'; /** @var Connection $connection */ $connection = static::getContainer()->get(Connection::class); $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'outsider-comp@test.local', '', 'Outsider', 'User', '[\"ROLE_ELEVE\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => $outsiderId, 'tid' => self::TENANT_ID], ); $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [ [ 'studentId' => $outsiderId, 'competencyEvaluationId' => $this->competencyEvaluation1Id, 'levelCode' => 'acquired', ], ], ], ]); self::assertResponseStatusCodeSame(400); } #[Test] public function putCompetencyResultsReturns400ForInvalidCompetencyEvaluationId(): void { $fakeCeId = (string) Uuid::uuid4(); $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [ [ 'studentId' => self::STUDENT_ID, 'competencyEvaluationId' => $fakeCeId, 'levelCode' => 'acquired', ], ], ], ]); self::assertResponseStatusCodeSame(400); } #[Test] public function putCompetencyResultsDeletesResultWhenLevelCodeIsNull(): void { // Verify the seeded result exists via the results grid $getClient = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $getClient->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $getClient->getResponse()->getContent(); /** @var array $before */ $before = json_decode($content, true, 512, JSON_THROW_ON_ERROR); $seededBefore = null; foreach ($before as $row) { if ($row['studentId'] === self::STUDENT_ID && $row['competencyEvaluationId'] === $this->competencyEvaluation1Id) { $seededBefore = $row; break; } } self::assertNotNull($seededBefore); self::assertSame('acquired', $seededBefore['levelCode']); // Send null levelCode to delete the result $putClient = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $putClient->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [ [ 'studentId' => self::STUDENT_ID, 'competencyEvaluationId' => $this->competencyEvaluation1Id, 'levelCode' => null, ], ], ], ]); self::assertResponseIsSuccessful(); // Verify the result is now null in the grid $verifyClient = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $verifyClient->request('GET', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Accept' => 'application/json'], ]); /** @var string $afterContent */ $afterContent = $verifyClient->getResponse()->getContent(); /** @var array $after */ $after = json_decode($afterContent, true, 512, JSON_THROW_ON_ERROR); $seededAfter = null; foreach ($after as $row) { if ($row['studentId'] === self::STUDENT_ID && $row['competencyEvaluationId'] === $this->competencyEvaluation1Id) { $seededAfter = $row; break; } } self::assertNotNull($seededAfter); self::assertNull($seededAfter['levelCode'] ?? null); } // ========================================================================= // POST /evaluations/{id}/competencies — Validation (error paths) // ========================================================================= #[Test] public function postLinkCompetenciesReturns400ForUnknownCompetencyId(): void { $fakeCompetencyId = (string) Uuid::uuid4(); $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'competencyIds' => [$fakeCompetencyId], ], ]); self::assertResponseStatusCodeSame(400); } #[Test] public function postLinkCompetenciesIsIdempotentForAlreadyLinkedCompetency(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); // Link the same competencies that are already linked $client->request('POST', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competencies', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'competencyIds' => [self::COMPETENCY1_ID], ], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(1, $data); self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']); } // ========================================================================= // AC6: Mode mixte — Competency results don't create grades // ========================================================================= #[Test] public function competencyResultsDoNotCreateGradeEntries(): void { /** @var Connection $connection */ $connection = static::getContainer()->get(Connection::class); // Count grades before saving competency results /** @var string $countBefore */ $countBefore = $connection->fetchOne( 'SELECT COUNT(*) FROM grades WHERE evaluation_id = :eid', ['eid' => (string) $this->evaluationId], ); $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('PUT', self::BASE_URL . '/evaluations/' . $this->evaluationId . '/competency-results', [ 'headers' => ['Content-Type' => 'application/json', 'Accept' => 'application/json'], 'json' => [ 'results' => [ [ 'studentId' => self::STUDENT_ID, 'competencyEvaluationId' => $this->competencyEvaluation1Id, 'levelCode' => 'exceeded', ], [ 'studentId' => self::STUDENT2_ID, 'competencyEvaluationId' => $this->competencyEvaluation2Id, 'levelCode' => 'in_progress', ], ], ], ]); self::assertResponseIsSuccessful(); // Count grades after — should be unchanged (competency results don't create grades) /** @var string $countAfter */ $countAfter = $connection->fetchOne( 'SELECT COUNT(*) FROM grades WHERE evaluation_id = :eid', ['eid' => (string) $this->evaluationId], ); self::assertSame($countBefore, $countAfter, 'Saving competency results must not create grade entries'); // Also verify no student_averages were created for this evaluation /** @var string $avgCount */ $avgCount = $connection->fetchOne( 'SELECT COUNT(*) FROM student_averages WHERE tenant_id = :tid AND subject_id = :sid', ['tid' => self::TENANT_ID, 'sid' => self::SUBJECT_ID], ); self::assertSame(0, (int) $avgCount, 'Competency results must not trigger average calculation'); } // ========================================================================= // GET /students/{id}/competency-progress — Auth & Happy path // ========================================================================= #[Test] public function getStudentCompetencyProgressReturns401WithoutAuthentication(): void { $client = static::createClient(); $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(401); } #[Test] public function getStudentCompetencyProgressReturns403ForOtherStudent(): void { $client = $this->createAuthenticatedClient(self::STUDENT2_ID, ['ROLE_ELEVE']); $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseStatusCodeSame(403); } #[Test] public function getStudentCompetencyProgressReturns200ForOwnData(): void { $client = $this->createAuthenticatedClient(self::STUDENT_ID, ['ROLE_ELEVE']); $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(1, $data); self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']); self::assertSame('acquired', $data[0]['currentLevelCode']); self::assertSame('Acquis', $data[0]['currentLevelName']); self::assertNotEmpty($data[0]['history']); } #[Test] public function getStudentCompetencyProgressReturns200ForTeacher(): void { $client = $this->createAuthenticatedClient(self::TEACHER_ID, ['ROLE_PROF']); $client->request('GET', self::BASE_URL . '/students/' . self::STUDENT_ID . '/competency-progress', [ 'headers' => ['Accept' => 'application/json'], ]); self::assertResponseIsSuccessful(); /** @var string $content */ $content = $client->getResponse()->getContent(); /** @var array $data */ $data = json_decode($content, true, 512, JSON_THROW_ON_ERROR); self::assertCount(1, $data); self::assertSame(self::COMPETENCY1_ID, $data[0]['competencyId']); } // ========================================================================= // Helpers // ========================================================================= /** * @param list $roles */ private function createAuthenticatedClient(string $userId, array $roles): Client { $client = static::createClient(); $user = new SecurityUser( userId: UserId::fromString($userId), email: 'test-comp@classeo.local', hashedPassword: '', tenantId: TenantId::fromString(self::TENANT_ID), roles: $roles, ); $client->loginUser($user, 'api'); return $client; } private function seedFixtures(): void { $container = static::getContainer(); /** @var Connection $connection */ $connection = $container->get(Connection::class); $tenantId = TenantId::fromString(self::TENANT_ID); $now = new DateTimeImmutable(); $schoolId = '550e8400-e29b-41d4-a716-ff6655440001'; $academicYearId = '550e8400-e29b-41d4-a716-ff6655440002'; // --- Users --- $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'teacher-comp@test.local', '', 'Test', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::TEACHER_ID, 'tid' => self::TENANT_ID], ); $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'other-teacher-comp@test.local', '', 'Other', 'Teacher', '[\"ROLE_PROF\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::OTHER_TEACHER_ID, 'tid' => self::TENANT_ID], ); $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'student1-comp@test.local', '', 'Alice', 'Dupont', '[\"ROLE_ELEVE\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::STUDENT_ID, 'tid' => self::TENANT_ID], ); $connection->executeStatement( "INSERT INTO users (id, tenant_id, email, hashed_password, first_name, last_name, roles, statut, created_at, updated_at) VALUES (:id, :tid, 'student2-comp@test.local', '', 'Bob', 'Martin', '[\"ROLE_ELEVE\"]', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::STUDENT2_ID, 'tid' => self::TENANT_ID], ); // --- School class --- $connection->executeStatement( "INSERT INTO school_classes (id, tenant_id, school_id, academic_year_id, name, status, created_at, updated_at) VALUES (:id, :tid, :sid, :ayid, 'Test-Comp-Class', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::CLASS_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId, 'ayid' => $academicYearId], ); // --- Subject --- $connection->executeStatement( "INSERT INTO subjects (id, tenant_id, school_id, name, code, status, created_at, updated_at) VALUES (:id, :tid, :sid, 'Test-Comp-Subject', 'TCOMP', 'active', NOW(), NOW()) ON CONFLICT (id) DO NOTHING", ['id' => self::SUBJECT_ID, 'tid' => self::TENANT_ID, 'sid' => $schoolId], ); // --- Class assignments (students assigned to class) --- $classAssignment1Id = (string) Uuid::uuid4(); $connection->executeStatement( 'INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) VALUES (:id, :tid, :uid, :cid, :ayid, NOW(), NOW(), NOW()) ON CONFLICT (user_id, academic_year_id) DO NOTHING', ['id' => $classAssignment1Id, 'tid' => self::TENANT_ID, 'uid' => self::STUDENT_ID, 'cid' => self::CLASS_ID, 'ayid' => $academicYearId], ); $classAssignment2Id = (string) Uuid::uuid4(); $connection->executeStatement( 'INSERT INTO class_assignments (id, tenant_id, user_id, school_class_id, academic_year_id, assigned_at, created_at, updated_at) VALUES (:id, :tid, :uid, :cid, :ayid, NOW(), NOW(), NOW()) ON CONFLICT (user_id, academic_year_id) DO NOTHING', ['id' => $classAssignment2Id, 'tid' => self::TENANT_ID, 'uid' => self::STUDENT2_ID, 'cid' => self::CLASS_ID, 'ayid' => $academicYearId], ); // --- Evaluation (owned by teacher) --- $evaluation = Evaluation::creer( tenantId: $tenantId, classId: ClassId::fromString(self::CLASS_ID), subjectId: SubjectId::fromString(self::SUBJECT_ID), teacherId: UserId::fromString(self::TEACHER_ID), title: 'Evaluation Competences', description: null, evaluationDate: new DateTimeImmutable('2026-03-15'), gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0), now: $now, ); $evaluation->pullDomainEvents(); /** @var EvaluationRepository $evalRepo */ $evalRepo = $container->get(EvaluationRepository::class); $evalRepo->save($evaluation); $this->evaluationId = $evaluation->id; // --- Competency framework (default for tenant) --- $connection->executeStatement( 'INSERT INTO competency_frameworks (id, tenant_id, name, is_default, created_at) VALUES (:id, :tid, :name, true, NOW()) ON CONFLICT (id) DO NOTHING', ['id' => self::FRAMEWORK_ID, 'tid' => self::TENANT_ID, 'name' => 'Socle commun'], ); // --- Competencies --- $connection->executeStatement( 'INSERT INTO competencies (id, framework_id, code, name, description, parent_id, sort_order) VALUES (:id, :fid, :code, :name, :desc, NULL, :sort) ON CONFLICT (id) DO NOTHING', ['id' => self::COMPETENCY1_ID, 'fid' => self::FRAMEWORK_ID, 'code' => 'C1', 'name' => 'Lire et comprendre', 'desc' => 'Capacite de lecture', 'sort' => 1], ); $connection->executeStatement( 'INSERT INTO competencies (id, framework_id, code, name, description, parent_id, sort_order) VALUES (:id, :fid, :code, :name, :desc, NULL, :sort) ON CONFLICT (id) DO NOTHING', ['id' => self::COMPETENCY2_ID, 'fid' => self::FRAMEWORK_ID, 'code' => 'C2', 'name' => 'Ecrire', 'desc' => 'Capacite d\'ecriture', 'sort' => 2], ); // --- Competency evaluations (link competencies to evaluation) --- $this->competencyEvaluation1Id = (string) Uuid::uuid4(); $connection->executeStatement( 'INSERT INTO competency_evaluations (id, evaluation_id, competency_id) VALUES (:id, :eid, :cid) ON CONFLICT (evaluation_id, competency_id) DO NOTHING', ['id' => $this->competencyEvaluation1Id, 'eid' => (string) $this->evaluationId, 'cid' => self::COMPETENCY1_ID], ); $this->competencyEvaluation2Id = (string) Uuid::uuid4(); $connection->executeStatement( 'INSERT INTO competency_evaluations (id, evaluation_id, competency_id) VALUES (:id, :eid, :cid) ON CONFLICT (evaluation_id, competency_id) DO NOTHING', ['id' => $this->competencyEvaluation2Id, 'eid' => (string) $this->evaluationId, 'cid' => self::COMPETENCY2_ID], ); // --- Student competency result (student1 + competency1 = acquired) --- $resultId = (string) Uuid::uuid4(); $connection->executeStatement( 'INSERT INTO student_competency_results (id, tenant_id, competency_evaluation_id, student_id, level_code, created_at, updated_at) VALUES (:id, :tid, :ceid, :sid, :level, NOW(), NOW()) ON CONFLICT (competency_evaluation_id, student_id) DO NOTHING', [ 'id' => $resultId, 'tid' => self::TENANT_ID, 'ceid' => $this->competencyEvaluation1Id, 'sid' => self::STUDENT_ID, 'level' => 'acquired', ], ); } }