feat: Calculer automatiquement les moyennes après chaque saisie de notes
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

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.
This commit is contained in:
2026-03-30 06:22:03 +02:00
parent b70d5ec2ad
commit aedde6707e
694 changed files with 109792 additions and 75 deletions

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Service;
use App\Scolarite\Domain\Model\Evaluation\ClassStatistics;
use function array_sum;
use function count;
use function intdiv;
use function round;
use function sort;
final class AverageCalculator
{
/**
* Moyenne matière pondérée par les coefficients, normalisée sur /20.
* Les notes absent/dispensé doivent être exclues en amont.
*
* Formule : Σ(note_sur_20 × coef) / Σ(coef)
*
* @param list<GradeEntry> $grades
*/
public function calculateSubjectAverage(array $grades): ?float
{
if ($grades === []) {
return null;
}
$sumWeighted = 0.0;
$sumCoef = 0.0;
foreach ($grades as $entry) {
$normalizedValue = $entry->gradeScale->convertTo20($entry->value);
$sumWeighted += $normalizedValue * $entry->coefficient->value;
$sumCoef += $entry->coefficient->value;
}
return round($sumWeighted / $sumCoef, 2);
}
/**
* Moyenne générale : moyenne arithmétique des moyennes matières.
* Les matières sans note sont exclues en amont.
*
* @param list<float> $subjectAverages
*/
public function calculateGeneralAverage(array $subjectAverages): ?float
{
if ($subjectAverages === []) {
return null;
}
return round(array_sum($subjectAverages) / count($subjectAverages), 2);
}
/**
* Statistiques de classe pour une évaluation : min, max, moyenne, médiane.
* Les absents et dispensés doivent être exclus en amont.
*
* @param list<float> $values
*/
public function calculateClassStatistics(array $values): ClassStatistics
{
if ($values === []) {
return new ClassStatistics(
average: null,
min: null,
max: null,
median: null,
gradedCount: 0,
);
}
sort($values);
$count = count($values);
return new ClassStatistics(
average: round(array_sum($values) / $count, 2),
min: $values[0],
max: $values[$count - 1],
median: $this->calculateMedian($values),
gradedCount: $count,
);
}
/**
* @param list<float> $sortedValues Valeurs déjà triées par ordre croissant
*/
private function calculateMedian(array $sortedValues): float
{
$count = count($sortedValues);
$middle = intdiv($count, 2);
if ($count % 2 === 0) {
return round(($sortedValues[$middle - 1] + $sortedValues[$middle]) / 2, 2);
}
return $sortedValues[$middle];
}
}