Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
11 KiB
11 KiB
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
- Note publiée → moyenne matière élève recalculée
- Moyenne générale élève recalculée
- Moyenne de classe pour l'évaluation recalculée
- Calcul < 10ms (NFR-P9)
AC2 : Calcul moyenne matière
- Formule : Σ(note × coef) / Σ(coef)
- Coefficients de chaque évaluation appliqués
- Notes "absent" et "dispensé" exclues
AC3 : Calcul moyenne générale
- Coefficients matières appliqués (si configurés)
- Si pas de coef matière : moyenne arithmétique des moyennes matières
- Matières sans note exclues
AC4 : Statistiques classe
- Moyenne de classe : min / max / moyenne / médiane
- Absents et dispensés exclus
AC5 : Recalcul sur modification
- Note modifiée → moyennes impactées recalculées immédiatement
- 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)
- T1.1 : Service
AverageCalculator - T1.2 : Calcul moyenne matière avec coefficients
- T1.3 : Calcul moyenne générale
- T1.4 : Calcul statistiques classe
- T1.5 : Tests unitaires
T2 : Domain Events triggers (AC: #5)
- T2.1 : Listener
OnNotePubliee→ recalcul - T2.2 : Listener
OnNoteModifiee→ recalcul - T2.3 : Tests intégration
T3 : Cache Redis (AC: #1)
- T3.1 : Cache moyennes fréquemment consultées
- T3.2 : Invalidation sur modification
- T3.3 : TTL configurable
- T3.4 : Tests
T4 : Stockage moyennes (AC: #1-4)
- T4.1 : Table
student_averagesdénormalisée - T4.2 : Table
class_statisticsdénormalisée - T4.3 : Mise à jour via events
- T4.4 : Tests intégration
T5 : API Endpoints (AC: #1-4)
- T5.1 :
GET /api/students/{id}/averages- Moyennes élève - T5.2 :
GET /api/classes/{id}/statistics- Stats classe - 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
-- 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
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
AverageCalculatordansDomain/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 ObjectClassStatisticset DTOGradeEntry. 19 tests unitaires. - T2 : Event handlers Symfony Messenger pour
NotesPubliees,NoteSaisie|NoteModifiee,EvaluationModifiee,EvaluationSupprimee. Logique commune extraite dansRecalculerMoyennesService. Requêtes batch viafindByEvaluations()(N+1 résolu). 8 tests unitaires. - T3 : Cache Redis via Symfony Cache pools (TTL 5 min configurable). Decorators
CachingStudentAverageRepositoryetCachingEvaluationStatisticsRepositoryavec 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-moyennespour 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)