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: RecalculerMoyennesOnEvaluationSupprimeeHandlerTest::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 RecalculerMoyennesOnEvaluationSupprimeeHandler( tenantContext: $tenantContext, evaluationRepository: $this->evaluationRepo, gradeRepository: $this->gradeRepo, evaluationStatisticsRepository: $this->evalStatsRepo, periodFinder: $periodFinder, service: $service, ); } #[Test] public function itDeletesEvaluationStatisticsOnDeletion(): void { $evaluationId = $this->seedPublishedEvaluationWithGrades( grades: [ [self::STUDENT_1, 14.0, GradeStatus::GRADED], [self::STUDENT_2, 10.0, GradeStatus::GRADED], ], ); // Pré-remplir les stats $this->evalStatsRepo->save($evaluationId, new ClassStatistics( average: 12.0, min: 10.0, max: 14.0, median: 12.0, gradedCount: 2, )); self::assertNotNull($this->evalStatsRepo->findByEvaluation($evaluationId)); ($this->handler)(new EvaluationSupprimee( evaluationId: $evaluationId, occurredOn: new DateTimeImmutable(), )); self::assertNull($this->evalStatsRepo->findByEvaluation($evaluationId)); } #[Test] public function itRecalculatesStudentAveragesAfterDeletion(): void { $tenantId = TenantId::fromString(self::TENANT_ID); // Première évaluation (sera supprimée) $evalToDelete = $this->seedPublishedEvaluationWithGrades( grades: [ [self::STUDENT_1, 10.0, GradeStatus::GRADED], ], ); // Deuxième évaluation (reste) $evalRemaining = $this->seedPublishedEvaluationWithGrades( grades: [ [self::STUDENT_1, 18.0, GradeStatus::GRADED], ], ); // Pré-remplir les moyennes (comme si les deux évaluations comptaient) $this->studentAvgRepo->saveSubjectAverage( $tenantId, UserId::fromString(self::STUDENT_1), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, 14.0, // (10+18)/2 2, ); // Supprimer la première évaluation (status DELETED mais encore accessible) $evaluation = $this->evaluationRepo->findById($evalToDelete, $tenantId); $evaluation->supprimer(new DateTimeImmutable()); $evaluation->pullDomainEvents(); $this->evaluationRepo->save($evaluation); ($this->handler)(new EvaluationSupprimee( evaluationId: $evalToDelete, occurredOn: new DateTimeImmutable(), )); // La moyenne doit être recalculée sans l'évaluation supprimée $subjectAvg = $this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_1), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, $tenantId, ); self::assertNotNull($subjectAvg); self::assertSame(18.0, $subjectAvg['average']); self::assertSame(1, $subjectAvg['gradeCount']); } #[Test] public function itDoesNothingWhenEvaluationNotFound(): void { $unknownId = EvaluationId::generate(); ($this->handler)(new EvaluationSupprimee( evaluationId: $unknownId, occurredOn: new DateTimeImmutable(), )); self::assertNull($this->evalStatsRepo->findByEvaluation($unknownId)); } #[Test] public function itOnlyDeletesStatsWhenGradesWereNotPublished(): void { $evaluationId = $this->seedUnpublishedEvaluationWithGrades( grades: [ [self::STUDENT_1, 14.0, GradeStatus::GRADED], ], ); // Pré-remplir des stats (cas hypothétique) $this->evalStatsRepo->save($evaluationId, new ClassStatistics( average: 14.0, min: 14.0, max: 14.0, median: 14.0, gradedCount: 1, )); ($this->handler)(new EvaluationSupprimee( evaluationId: $evaluationId, occurredOn: new DateTimeImmutable(), )); // Stats supprimées self::assertNull($this->evalStatsRepo->findByEvaluation($evaluationId)); // Pas de recalcul de moyennes élèves (notes non publiées) self::assertNull($this->studentAvgRepo->findSubjectAverage( UserId::fromString(self::STUDENT_1), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, TenantId::fromString(self::TENANT_ID), )); } #[Test] public function itRecalculatesGeneralAverageAfterDeletion(): void { $tenantId = TenantId::fromString(self::TENANT_ID); $evaluationId = $this->seedPublishedEvaluationWithGrades( grades: [ [self::STUDENT_1, 14.0, GradeStatus::GRADED], ], ); // Pré-remplir $this->studentAvgRepo->saveSubjectAverage( $tenantId, UserId::fromString(self::STUDENT_1), SubjectId::fromString(self::SUBJECT_ID), self::PERIOD_ID, 14.0, 1, ); $this->studentAvgRepo->saveGeneralAverage( $tenantId, UserId::fromString(self::STUDENT_1), self::PERIOD_ID, 14.0, ); // Supprimer l'évaluation $evaluation = $this->evaluationRepo->findById($evaluationId, $tenantId); $evaluation->supprimer(new DateTimeImmutable()); $evaluation->pullDomainEvents(); $this->evaluationRepo->save($evaluation); ($this->handler)(new EvaluationSupprimee( evaluationId: $evaluationId, occurredOn: new DateTimeImmutable(), )); // Plus aucune note publiée → moyennes supprimées $generalAvg = $this->studentAvgRepo->findGeneralAverageForStudent( UserId::fromString(self::STUDENT_1), self::PERIOD_ID, $tenantId, ); self::assertNull($generalAvg); } /** * @param list $grades */ private function seedPublishedEvaluationWithGrades( array $grades, float $coefficient = 1.0, ): EvaluationId { return $this->seedEvaluationWithGrades($grades, $coefficient, published: true); } /** * @param list $grades */ private function seedUnpublishedEvaluationWithGrades( array $grades, float $coefficient = 1.0, ): EvaluationId { return $this->seedEvaluationWithGrades($grades, $coefficient, published: false); } /** * @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; } }