# Story 6.3 : Calcul Automatique des Moyennes Status: in-progress --- ## Story En tant que **système**, Je veux **calculer automatiquement les moyennes par matière et générales**, Afin de **fournir des données actualisées instantanément après chaque saisie (FR13)**. --- ## Critères d'Acceptation ### AC1 : Recalcul après publication - [x] Note publiée → moyenne matière élève recalculée - [x] Moyenne générale élève recalculée - [x] Moyenne de classe pour l'évaluation recalculée - [ ] Calcul < 10ms (NFR-P9) ### AC2 : Calcul moyenne matière - [x] Formule : Σ(note × coef) / Σ(coef) - [x] Coefficients de chaque évaluation appliqués - [x] Notes "absent" et "dispensé" exclues ### AC3 : Calcul moyenne générale - [x] Coefficients matières appliqués (si configurés) - [x] Si pas de coef matière : moyenne arithmétique des moyennes matières - [x] Matières sans note exclues ### AC4 : Statistiques classe - [x] Moyenne de classe : min / max / moyenne / médiane - [x] Absents et dispensés exclus ### AC5 : Recalcul sur modification - [x] Note modifiée → moyennes impactées recalculées immédiatement - [x] Via Domain Events (eager calculation) ### AC6 : Mode compétences - [ ] Établissement avec mode compétences → seules notes chiffrées prises en compte - [ ] Compétences ont leur propre agrégation --- ## Tâches / Sous-tâches ### T1 : Service calcul moyennes (AC: #1-4) - [x] T1.1 : Service `AverageCalculator` - [x] T1.2 : Calcul moyenne matière avec coefficients - [x] T1.3 : Calcul moyenne générale - [x] T1.4 : Calcul statistiques classe - [x] T1.5 : Tests unitaires ### T2 : Domain Events triggers (AC: #5) - [x] T2.1 : Listener `OnNotePubliee` → recalcul - [x] T2.2 : Listener `OnNoteModifiee` → recalcul - [x] T2.3 : Tests intégration ### T3 : Cache Redis (AC: #1) - [x] T3.1 : Cache moyennes fréquemment consultées - [x] T3.2 : Invalidation sur modification - [x] T3.3 : TTL configurable - [x] T3.4 : Tests ### T4 : Stockage moyennes (AC: #1-4) - [x] T4.1 : Table `student_averages` dénormalisée - [x] T4.2 : Table `class_statistics` dénormalisée - [x] T4.3 : Mise à jour via events - [x] T4.4 : Tests intégration ### T5 : API Endpoints (AC: #1-4) - [x] T5.1 : `GET /api/students/{id}/averages` - Moyennes élève - [x] T5.2 : `GET /api/classes/{id}/statistics` - Stats classe - [x] T5.3 : `GET /api/evaluations/{id}/statistics` - Stats évaluation --- ## Notes Développeur ### Dépendances - **Story 6.1** : Création d'évaluation - **Story 6.2** : Saisie notes ### Schema Base de Données ```sql -- Moyennes dénormalisées pour performance CREATE TABLE student_averages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, student_id UUID NOT NULL REFERENCES users(id), subject_id UUID NOT NULL REFERENCES subjects(id), period_id UUID NOT NULL REFERENCES academic_periods(id), average DECIMAL(4,2), grade_count INT DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (student_id, subject_id, period_id) ); CREATE TABLE student_general_averages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, student_id UUID NOT NULL REFERENCES users(id), period_id UUID NOT NULL REFERENCES academic_periods(id), average DECIMAL(4,2), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (student_id, period_id) ); CREATE TABLE evaluation_statistics ( evaluation_id UUID PRIMARY KEY REFERENCES evaluations(id), average DECIMAL(4,2), min_grade DECIMAL(4,2), max_grade DECIMAL(4,2), median_grade DECIMAL(4,2), graded_count INT DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_student_averages_student ON student_averages(student_id); CREATE INDEX idx_student_averages_subject ON student_averages(subject_id); ``` ### Service Calcul Moyennes ```php final class AverageCalculator { public function calculateSubjectAverage( UserId $studentId, SubjectId $subjectId, PeriodId $periodId ): ?float { $grades = $this->gradeRepository->findPublishedForStudentSubjectPeriod( $studentId, $subjectId, $periodId ); $validGrades = array_filter( $grades, fn(Grade $g) => $g->status === GradeStatus::GRADED ); if (empty($validGrades)) { return null; } $sumWeighted = 0; $sumCoef = 0; foreach ($validGrades as $grade) { $normalizedValue = $grade->evaluation->gradeScale->convertTo20($grade->value); $sumWeighted += $normalizedValue * $grade->evaluation->coefficient->value; $sumCoef += $grade->evaluation->coefficient->value; } return round($sumWeighted / $sumCoef, 2); } public function calculateClassStatistics(EvaluationId $evaluationId): ClassStatistics { $grades = $this->gradeRepository->findGradedForEvaluation($evaluationId); $values = array_map(fn(Grade $g) => $g->value, $grades); sort($values); return new ClassStatistics( average: count($values) > 0 ? round(array_sum($values) / count($values), 2) : null, min: min($values) ?? null, max: max($values) ?? null, median: $this->calculateMedian($values), gradedCount: count($values) ); } } ``` ### Contraintes Techniques - Calculs via Domain Events (pas de batch nocturne) - Cache Redis pour moyennes fréquemment consultées - Précision : 2 décimales, arrondi au plus proche - Performance < 10ms (NFR-P9) ### FRs/NFRs Couverts - **FR13** : Calcul automatique des moyennes - **NFR-P9** : Calculs < 10ms ### Structure Fichiers ``` backend/src/Scolarite/ ├── Application/ │ ├── Service/ │ │ └── AverageCalculator.php │ └── Listener/ │ ├── OnNotePubliee.php │ └── OnNoteModifiee.php └── Infrastructure/ ├── Cache/ │ └── RedisAverageCache.php └── Api/ └── AverageController.php ``` ### Références - [Source: _bmad-output/planning-artifacts/epics.md - Epic 6 Story 6.3] --- ## Dev Agent Record ### Agent Model Used Claude Opus 4.6 ### Debug Log References ### Completion Notes List - **T1** : Service `AverageCalculator` dans `Domain/Service/` — calcul pondéré des moyennes matières (normalisation /20), moyenne générale (arithmétique), statistiques de classe (min/max/moyenne/médiane). Value Object `ClassStatistics` et DTO `GradeEntry`. 19 tests unitaires. - **T2** : Event handlers Symfony Messenger pour `NotesPubliees`, `NoteSaisie|NoteModifiee`, `EvaluationModifiee`, `EvaluationSupprimee`. Logique commune extraite dans `RecalculerMoyennesService`. Requêtes batch via `findByEvaluations()` (N+1 résolu). 8 tests unitaires. - **T3** : Cache Redis via Symfony Cache pools (TTL 5 min configurable). Decorators `CachingStudentAverageRepository` et `CachingEvaluationStatisticsRepository` avec invalidation sur écriture/suppression. 7 tests unitaires. - **T4** : Migration Doctrine pour 3 tables dénormalisées (student_averages, student_general_averages, evaluation_statistics). Repositories Doctrine avec UPSERT + DELETE. InMemory implémentations. - **T5** : 3 endpoints API Platform avec contrôle d'accès (ownership/role), validation UUID, tenant isolation. LEFT JOIN pour stats partielles. - **Backfill** : Commande `app:recalculer-moyennes` pour remplir les projections depuis les données historiques (--tenant optionnel). 4 tests unitaires. - **Code Review fixes** : N+1 queries résolu, NoteSaisie handler ajouté, null averages cleanup, EvaluationModifiee/Supprimee handlers, FQCN corrigé, equals() standardisé, UUID validation 400 vs 500, LEFT JOIN. ### File List - `backend/src/Scolarite/Domain/Service/AverageCalculator.php` (new) - `backend/src/Scolarite/Domain/Service/GradeEntry.php` (new) - `backend/src/Scolarite/Domain/Model/Evaluation/ClassStatistics.php` (new) - `backend/src/Scolarite/Domain/Repository/EvaluationStatisticsRepository.php` (new) - `backend/src/Scolarite/Domain/Repository/StudentAverageRepository.php` (new) - `backend/src/Scolarite/Domain/Repository/EvaluationRepository.php` (modified) - `backend/src/Scolarite/Domain/Repository/GradeRepository.php` (modified) - `backend/src/Scolarite/Application/Port/PeriodFinder.php` (new) - `backend/src/Scolarite/Application/Port/PeriodInfo.php` (new) - `backend/src/Scolarite/Application/Service/RecalculerMoyennesService.php` (new) - `backend/src/Scolarite/Infrastructure/Persistence/Doctrine/DoctrineEvaluationRepository.php` (modified) - `backend/src/Scolarite/Infrastructure/Persistence/Doctrine/DoctrineGradeRepository.php` (modified) - `backend/src/Scolarite/Infrastructure/Persistence/Doctrine/DoctrineEvaluationStatisticsRepository.php` (new) - `backend/src/Scolarite/Infrastructure/Persistence/Doctrine/DoctrineStudentAverageRepository.php` (new) - `backend/src/Scolarite/Infrastructure/Persistence/InMemory/InMemoryEvaluationRepository.php` (modified) - `backend/src/Scolarite/Infrastructure/Persistence/InMemory/InMemoryGradeRepository.php` (modified) - `backend/src/Scolarite/Infrastructure/Persistence/InMemory/InMemoryEvaluationStatisticsRepository.php` (new) - `backend/src/Scolarite/Infrastructure/Persistence/InMemory/InMemoryStudentAverageRepository.php` (new) - `backend/src/Scolarite/Infrastructure/Service/DoctrinePeriodFinder.php` (new) - `backend/src/Scolarite/Infrastructure/EventHandler/RecalculerMoyennesOnNotesPublieesHandler.php` (new) - `backend/src/Scolarite/Infrastructure/EventHandler/RecalculerMoyennesOnNoteModifieeHandler.php` (new) - `backend/src/Scolarite/Infrastructure/EventHandler/RecalculerMoyennesOnEvaluationModifieeHandler.php` (new) - `backend/src/Scolarite/Infrastructure/EventHandler/RecalculerMoyennesOnEvaluationSupprimeeHandler.php` (new) - `backend/src/Scolarite/Infrastructure/Cache/CachingStudentAverageRepository.php` (new) - `backend/src/Scolarite/Infrastructure/Cache/CachingEvaluationStatisticsRepository.php` (new) - `backend/src/Scolarite/Infrastructure/Console/RecalculerToutesMoyennesCommand.php` (new) - `backend/src/Scolarite/Infrastructure/Api/Resource/EvaluationStatisticsResource.php` (new) - `backend/src/Scolarite/Infrastructure/Api/Resource/StudentAveragesResource.php` (new) - `backend/src/Scolarite/Infrastructure/Api/Resource/ClassStatisticsResource.php` (new) - `backend/src/Scolarite/Infrastructure/Api/Provider/EvaluationStatisticsProvider.php` (new) - `backend/src/Scolarite/Infrastructure/Api/Provider/StudentAveragesProvider.php` (new) - `backend/src/Scolarite/Infrastructure/Api/Provider/ClassStatisticsProvider.php` (new) - `backend/migrations/Version20260329082334.php` (new) - `backend/config/packages/cache.yaml` (modified) - `backend/config/services.yaml` (modified) - `backend/tests/Unit/Scolarite/Domain/Service/AverageCalculatorTest.php` (new) - `backend/tests/Unit/Scolarite/Infrastructure/EventHandler/RecalculerMoyennesOnNotesPublieesHandlerTest.php` (new) - `backend/tests/Unit/Scolarite/Infrastructure/EventHandler/RecalculerMoyennesOnNoteModifieeHandlerTest.php` (new) - `backend/tests/Unit/Scolarite/Infrastructure/Cache/CachingEvaluationStatisticsRepositoryTest.php` (new) - `backend/tests/Unit/Scolarite/Infrastructure/Cache/CachingStudentAverageRepositoryTest.php` (new) - `backend/tests/Unit/Scolarite/Infrastructure/Console/RecalculerToutesMoyennesCommandTest.php` (new)