evaluationRepo = new InMemoryEvaluationRepository(); $this->gradeRepo = new InMemoryGradeRepository(); $this->evalStatsRepo = new InMemoryEvaluationStatisticsRepository(); $this->studentAvgRepo = new InMemoryStudentAverageRepository(); $tenantContext = new TenantContext(); $tenantContext->setCurrentTenant(new TenantConfig( tenantId: InfraTenantId::fromString(self::TENANT_ID), subdomain: 'test', databaseUrl: 'postgresql://test', )); $periodFinder = new class implements PeriodFinder { public function findForDate(DateTimeImmutable $date, TenantId $tenantId): ?PeriodInfo { return new PeriodInfo( periodId: RecalculerMoyennesOnNotesPublieesHandlerTest::PERIOD_ID, startDate: new DateTimeImmutable('2026-01-01'), endDate: new DateTimeImmutable('2026-03-31'), ); } }; $service = new RecalculerMoyennesService( evaluationRepository: $this->evaluationRepo, gradeRepository: $this->gradeRepo, evaluationStatisticsRepository: $this->evalStatsRepo, studentAverageRepository: $this->studentAvgRepo, periodFinder: $periodFinder, calculator: new AverageCalculator(), ); $this->handler = new RecalculerMoyennesOnNotesPublieesHandler( tenantContext: $tenantContext, evaluationRepository: $this->evaluationRepo, gradeRepository: $this->gradeRepo, periodFinder: $periodFinder, service: $service, ); } #[Test] public function itCalculatesEvaluationStatisticsOnPublication(): void { $evaluationId = $this->seedEvaluationWithGrades( grades: [ [self::STUDENT_1, 14.0, GradeStatus::GRADED], [self::STUDENT_2, 8.0, GradeStatus::GRADED], ], ); ($this->handler)(new NotesPubliees( evaluationId: $evaluationId, occurredOn: new DateTimeImmutable(), )); $stats = $this->evalStatsRepo->findByEvaluation($evaluationId); self::assertNotNull($stats); self::assertSame(11.0, $stats->average); self::assertSame(8.0, $stats->min); self::assertSame(14.0, $stats->max); self::assertSame(11.0, $stats->median); self::assertSame(2, $stats->gradedCount); } #[Test] public function itExcludesAbsentAndDispensedFromStatistics(): void { $evaluationId = $this->seedEvaluationWithGrades( grades: [ [self::STUDENT_1, 16.0, GradeStatus::GRADED], [self::STUDENT_2, null, GradeStatus::ABSENT], ], ); ($this->handler)(new NotesPubliees( evaluationId: $evaluationId, occurredOn: new DateTimeImmutable(), )); $stats = $this->evalStatsRepo->findByEvaluation($evaluationId); self::assertNotNull($stats); self::assertSame(16.0, $stats->average); self::assertSame(1, $stats->gradedCount); } #[Test] public function itCalculatesSubjectAverageForEachStudent(): void { $evaluationId = $this->seedEvaluationWithGrades( grades: [ [self::STUDENT_1, 15.0, GradeStatus::GRADED], [self::STUDENT_2, 10.0, GradeStatus::GRADED], ], ); ($this->handler)(new NotesPubliees( evaluationId: $evaluationId, occurredOn: new DateTimeImmutable(), )); $student1Avg = $this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_1), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, TenantId::fromString(self::TENANT_ID), ); self::assertNotNull($student1Avg); self::assertSame(15.0, $student1Avg['average']); self::assertSame(1, $student1Avg['gradeCount']); $student2Avg = $this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_2), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, TenantId::fromString(self::TENANT_ID), ); self::assertNotNull($student2Avg); self::assertSame(10.0, $student2Avg['average']); } #[Test] public function itCalculatesWeightedSubjectAverageAcrossMultipleEvaluations(): void { // Première évaluation publiée (coef 2) $eval1Id = $this->seedEvaluationWithGrades( grades: [[self::STUDENT_1, 16.0, GradeStatus::GRADED]], coefficient: 2.0, published: true, ); // Publier la première évaluation d'abord ($this->handler)(new NotesPubliees( evaluationId: $eval1Id, occurredOn: new DateTimeImmutable(), )); // Deuxième évaluation publiée (coef 1) $eval2Id = $this->seedEvaluationWithGrades( grades: [[self::STUDENT_1, 10.0, GradeStatus::GRADED]], coefficient: 1.0, published: true, ); ($this->handler)(new NotesPubliees( evaluationId: $eval2Id, occurredOn: new DateTimeImmutable(), )); $student1Avg = $this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_1), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, TenantId::fromString(self::TENANT_ID), ); self::assertNotNull($student1Avg); // (16×2 + 10×1) / (2+1) = 42/3 = 14.0 self::assertSame(14.0, $student1Avg['average']); self::assertSame(2, $student1Avg['gradeCount']); } #[Test] public function itCalculatesGeneralAverage(): void { $evaluationId = $this->seedEvaluationWithGrades( grades: [[self::STUDENT_1, 14.0, GradeStatus::GRADED]], ); ($this->handler)(new NotesPubliees( evaluationId: $evaluationId, occurredOn: new DateTimeImmutable(), )); $generalAvg = $this->studentAvgRepo->findGeneralAverageForStudent( UserId::fromString(self::STUDENT_1), self::PERIOD_ID, TenantId::fromString(self::TENANT_ID), ); self::assertSame(14.0, $generalAvg); } #[Test] public function itDoesNothingWhenEvaluationNotFound(): void { $unknownId = EvaluationId::generate(); ($this->handler)(new NotesPubliees( evaluationId: $unknownId, occurredOn: new DateTimeImmutable(), )); self::assertNull($this->evalStatsRepo->findByEvaluation($unknownId)); } /** * @param list $grades */ private function seedEvaluationWithGrades( array $grades, float $coefficient = 1.0, bool $published = true, ): EvaluationId { $tenantId = TenantId::fromString(self::TENANT_ID); $now = new DateTimeImmutable(); $evaluation = Evaluation::creer( tenantId: $tenantId, classId: ClassId::fromString(self::CLASS_ID), subjectId: SubjectId::fromString(self::SUBJECT_ID), teacherId: UserId::fromString(self::TEACHER_ID), title: 'Test Evaluation', description: null, evaluationDate: new DateTimeImmutable('2026-02-15'), gradeScale: new GradeScale(20), coefficient: new Coefficient($coefficient), now: $now, ); if ($published) { $evaluation->publierNotes($now); } $evaluation->pullDomainEvents(); $this->evaluationRepo->save($evaluation); foreach ($grades as [$studentId, $value, $status]) { $grade = Grade::saisir( tenantId: $tenantId, evaluationId: $evaluation->id, studentId: UserId::fromString($studentId), value: $value !== null ? new GradeValue($value) : null, status: $status, gradeScale: new GradeScale(20), createdBy: UserId::fromString(self::TEACHER_ID), now: $now, ); $grade->pullDomainEvents(); $this->gradeRepo->save($grade); } return $evaluation->id; } }