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: RecalculerMoyennesOnNoteModifieeHandlerTest::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 RecalculerMoyennesOnNoteModifieeHandler( tenantContext: $tenantContext, evaluationRepository: $this->evaluationRepo, gradeRepository: $this->gradeRepo, periodFinder: $periodFinder, service: $service, ); } #[Test] public function itRecalculatesStatisticsWhenGradeModifiedAfterPublication(): void { $tenantId = TenantId::fromString(self::TENANT_ID); $now = new DateTimeImmutable(); $evaluation = $this->seedPublishedEvaluation(); // Deux notes initiales $grade1 = $this->seedGrade($evaluation->id, self::STUDENT_ID, 14.0, GradeStatus::GRADED); $this->seedGrade($evaluation->id, '77777777-7777-7777-7777-777777777777', 10.0, GradeStatus::GRADED); // Simuler modification de la note $grade1->modifier( value: new GradeValue(18.0), status: GradeStatus::GRADED, gradeScale: new GradeScale(20), modifiedBy: UserId::fromString(self::TEACHER_ID), now: $now, ); $grade1->pullDomainEvents(); $this->gradeRepo->save($grade1); $event = new NoteModifiee( gradeId: $grade1->id, evaluationId: (string) $evaluation->id, oldValue: 14.0, newValue: 18.0, oldStatus: 'graded', newStatus: 'graded', modifiedBy: self::TEACHER_ID, occurredOn: $now, ); ($this->handler)($event); // Statistiques recalculées $stats = $this->evalStatsRepo->findByEvaluation($evaluation->id); self::assertNotNull($stats); self::assertSame(14.0, $stats->average); // (18+10)/2 self::assertSame(10.0, $stats->min); self::assertSame(18.0, $stats->max); // Moyenne matière recalculée pour l'élève $subjectAvg = $this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_ID), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, $tenantId, ); self::assertNotNull($subjectAvg); self::assertSame(18.0, $subjectAvg['average']); } #[Test] public function itDoesNothingWhenGradesNotYetPublished(): void { $now = new DateTimeImmutable(); $tenantId = TenantId::fromString(self::TENANT_ID); // Évaluation NON publiée $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', description: null, evaluationDate: new DateTimeImmutable('2026-02-15'), gradeScale: new GradeScale(20), coefficient: new Coefficient(1.0), now: $now, ); $evaluation->pullDomainEvents(); $this->evaluationRepo->save($evaluation); $grade = $this->seedGrade($evaluation->id, self::STUDENT_ID, 14.0, GradeStatus::GRADED); $event = new NoteModifiee( gradeId: $grade->id, evaluationId: (string) $evaluation->id, oldValue: 10.0, newValue: 14.0, oldStatus: 'graded', newStatus: 'graded', modifiedBy: self::TEACHER_ID, occurredOn: $now, ); ($this->handler)($event); self::assertNull($this->evalStatsRepo->findByEvaluation($evaluation->id)); } #[Test] public function itRecalculatesOnNoteSaisieWhenAlreadyPublished(): void { $tenantId = TenantId::fromString(self::TENANT_ID); $now = new DateTimeImmutable(); $evaluation = $this->seedPublishedEvaluation(); $grade = $this->seedGrade($evaluation->id, self::STUDENT_ID, 16.0, GradeStatus::GRADED); $event = new NoteSaisie( gradeId: $grade->id, evaluationId: (string) $evaluation->id, studentId: self::STUDENT_ID, value: 16.0, status: 'graded', createdBy: self::TEACHER_ID, occurredOn: $now, ); ($this->handler)($event); $stats = $this->evalStatsRepo->findByEvaluation($evaluation->id); self::assertNotNull($stats); self::assertSame(16.0, $stats->average); $subjectAvg = $this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_ID), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, $tenantId, ); self::assertNotNull($subjectAvg); self::assertSame(16.0, $subjectAvg['average']); } #[Test] public function itDoesNothingWhenGradeNotFound(): void { $now = new DateTimeImmutable(); $evaluation = $this->seedPublishedEvaluation(); $event = new NoteModifiee( gradeId: GradeId::generate(), evaluationId: (string) $evaluation->id, oldValue: 10.0, newValue: 14.0, oldStatus: 'graded', newStatus: 'graded', modifiedBy: self::TEACHER_ID, occurredOn: $now, ); ($this->handler)($event); // Les stats sont recalculées (car l'évaluation est publiée), // mais pas de moyenne élève (grade introuvable) $subjectAvg = $this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_ID), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, TenantId::fromString(self::TENANT_ID), ); self::assertNull($subjectAvg); } #[Test] public function itDoesNothingWhenEvaluationNotFound(): void { $now = new DateTimeImmutable(); $unknownEvalId = EvaluationId::generate(); $event = new NoteModifiee( gradeId: GradeId::generate(), evaluationId: (string) $unknownEvalId, oldValue: 10.0, newValue: 14.0, oldStatus: 'graded', newStatus: 'graded', modifiedBy: self::TEACHER_ID, occurredOn: $now, ); ($this->handler)($event); self::assertNull($this->evalStatsRepo->findByEvaluation($unknownEvalId)); } private function seedPublishedEvaluation(): Evaluation { $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(1.0), now: $now, ); $evaluation->publierNotes($now); $evaluation->pullDomainEvents(); $this->evaluationRepo->save($evaluation); return $evaluation; } private function seedGrade( EvaluationId $evaluationId, string $studentId, ?float $value, GradeStatus $status, ): Grade { $tenantId = TenantId::fromString(self::TENANT_ID); $now = new DateTimeImmutable(); $grade = Grade::saisir( tenantId: $tenantId, evaluationId: $evaluationId, 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 $grade; } }