evaluationRepository = new InMemoryEvaluationRepository(); $this->gradeRepository = new InMemoryGradeRepository(); $this->statisticsRepository = new InMemoryEvaluationStatisticsRepository(); $this->now = new DateTimeImmutable('2026-04-06 14:00:00'); } #[Test] public function itReturnsEmptyWhenParentHasNoChildren(): void { $handler = $this->createHandler(children: []); $result = $handler(new GetChildrenGradesSummaryQuery( parentId: self::PARENT_ID, tenantId: self::TENANT_ID, )); self::assertSame([], $result); } #[Test] public function itComputesAveragesFromVisibleGrades(): void { // Maths: 16/20 coeff 2 + 12/20 coeff 1 → weighted = (32+12)/3 = 14.67 $eval1 = $this->givenPublishedEvaluation( title: 'DS Maths', subjectId: self::SUBJECT_MATH_ID, publishedAt: '2026-04-03 10:00:00', coefficient: 2.0, ); $this->givenGrade($eval1, self::CHILD_A_ID, 16.0); $eval2 = $this->givenPublishedEvaluation( title: 'Contrôle Maths', subjectId: self::SUBJECT_MATH_ID, publishedAt: '2026-04-03 12:00:00', coefficient: 1.0, ); $this->givenGrade($eval2, self::CHILD_A_ID, 12.0); // Français: 15/20 coeff 1 → 15.0 $eval3 = $this->givenPublishedEvaluation( title: 'Dictée', subjectId: self::SUBJECT_FRENCH_ID, publishedAt: '2026-04-03 14:00:00', coefficient: 1.0, ); $this->givenGrade($eval3, self::CHILD_A_ID, 15.0); $handler = $this->createHandler( children: [ ['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'], ], ); $result = $handler(new GetChildrenGradesSummaryQuery( parentId: self::PARENT_ID, tenantId: self::TENANT_ID, )); self::assertCount(1, $result); self::assertSame(self::CHILD_A_ID, $result[0]->childId); self::assertCount(2, $result[0]->subjectAverages); // General = mean of subject averages = (14.67 + 15.0) / 2 = 14.84 self::assertNotNull($result[0]->generalAverage); self::assertEqualsWithDelta(14.84, $result[0]->generalAverage, 0.01); } #[Test] public function itRespectsDelayAndExcludesRecentGradesFromAverages(): void { // Visible grade (48h ago) $evalOld = $this->givenPublishedEvaluation( title: 'Ancien', subjectId: self::SUBJECT_MATH_ID, publishedAt: '2026-04-04 10:00:00', ); $this->givenGrade($evalOld, self::CHILD_A_ID, 10.0); // Not yet visible (12h ago, within 24h delay) $evalRecent = $this->givenPublishedEvaluation( title: 'Récent', subjectId: self::SUBJECT_MATH_ID, publishedAt: '2026-04-06 02:00:00', ); $this->givenGrade($evalRecent, self::CHILD_A_ID, 20.0); $handler = $this->createHandler( children: [ ['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'], ], ); $result = $handler(new GetChildrenGradesSummaryQuery( parentId: self::PARENT_ID, tenantId: self::TENANT_ID, )); self::assertCount(1, $result); // Only the old grade (10.0) should be in the average, not the recent one (20.0) self::assertSame(10.0, $result[0]->generalAverage); } #[Test] public function itReturnsNullAverageWhenNoVisibleGrades(): void { $handler = $this->createHandler( children: [ ['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'], ], ); $result = $handler(new GetChildrenGradesSummaryQuery( parentId: self::PARENT_ID, tenantId: self::TENANT_ID, )); self::assertCount(1, $result); self::assertSame([], $result[0]->subjectAverages); self::assertNull($result[0]->generalAverage); } #[Test] public function itReturnsNullAverageWhenAllGradesHaveNullValue(): void { // Grades with null value (status ABSENT / DISPENSED) should not contribute to averages $eval1 = $this->givenPublishedEvaluation( title: 'DS Maths', subjectId: self::SUBJECT_MATH_ID, publishedAt: '2026-04-03 10:00:00', ); $this->givenGradeWithStatus($eval1, self::CHILD_A_ID, GradeStatus::ABSENT); $eval2 = $this->givenPublishedEvaluation( title: 'Dictee', subjectId: self::SUBJECT_FRENCH_ID, publishedAt: '2026-04-03 12:00:00', ); $this->givenGradeWithStatus($eval2, self::CHILD_A_ID, GradeStatus::DISPENSED); $handler = $this->createHandler( children: [ ['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'], ], ); $result = $handler(new GetChildrenGradesSummaryQuery( parentId: self::PARENT_ID, tenantId: self::TENANT_ID, )); self::assertCount(1, $result); self::assertSame([], $result[0]->subjectAverages); self::assertNull($result[0]->generalAverage); } #[Test] public function itReturnsAveragesForMultipleChildren(): void { $evalA = $this->givenPublishedEvaluation( title: 'Maths A', publishedAt: '2026-04-03 10:00:00', ); $this->givenGrade($evalA, self::CHILD_A_ID, 16.0); $evalB = $this->givenPublishedEvaluation( title: 'Maths B', classId: '550e8400-e29b-41d4-a716-446655440021', publishedAt: '2026-04-03 10:00:00', ); $this->givenGrade($evalB, self::CHILD_B_ID, 8.0); $handler = $this->createHandler( children: [ ['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'], ['studentId' => self::CHILD_B_ID, 'firstName' => 'Lucas', 'lastName' => 'Dupont'], ], ); $result = $handler(new GetChildrenGradesSummaryQuery( parentId: self::PARENT_ID, tenantId: self::TENANT_ID, )); self::assertCount(2, $result); self::assertSame(16.0, $result[0]->generalAverage); self::assertSame(8.0, $result[1]->generalAverage); } /** * @param array $children */ private function createHandler( array $children = [], ): GetChildrenGradesSummaryHandler { $parentChildrenReader = new class($children) implements ParentChildrenReader { /** @param array $children */ public function __construct(private readonly array $children) { } public function childrenOf(string $guardianId, TenantId $tenantId): array { return $this->children; } }; $displayReader = new class implements ScheduleDisplayReader { public function subjectDisplay(string $tenantId, string ...$subjectIds): array { $map = []; foreach ($subjectIds as $id) { $map[$id] = ['name' => 'Mathématiques', 'color' => '#3b82f6']; } return $map; } public function teacherNames(string $tenantId, string ...$teacherIds): array { return []; } }; $clock = new class($this->now) implements Clock { public function __construct(private readonly DateTimeImmutable $now) { } public function now(): DateTimeImmutable { return $this->now; } }; $delayReader = new class implements ParentGradeDelayReader { public function delayHoursForTenant(TenantId $tenantId): int { return 24; } }; $gradesHandler = new GetChildrenGradesHandler( $parentChildrenReader, $this->evaluationRepository, $this->gradeRepository, $this->statisticsRepository, $displayReader, new VisibiliteNotesPolicy($clock), $delayReader, ); return new GetChildrenGradesSummaryHandler($gradesHandler); } protected function evaluationRepository(): InMemoryEvaluationRepository { return $this->evaluationRepository; } protected function gradeRepository(): InMemoryGradeRepository { return $this->gradeRepository; } }