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,113 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Infrastructure\Cache;
use App\Scolarite\Domain\Model\Evaluation\ClassStatistics;
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
use App\Scolarite\Infrastructure\Cache\CachingEvaluationStatisticsRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationStatisticsRepository;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
final class CachingEvaluationStatisticsRepositoryTest extends TestCase
{
private InMemoryEvaluationStatisticsRepository $inner;
private CachingEvaluationStatisticsRepository $cached;
protected function setUp(): void
{
$this->inner = new InMemoryEvaluationStatisticsRepository();
$this->cached = new CachingEvaluationStatisticsRepository(
inner: $this->inner,
cache: new ArrayAdapter(),
);
}
#[Test]
public function itCachesStatisticsOnRead(): void
{
$evaluationId = EvaluationId::generate();
$stats = new ClassStatistics(average: 14.5, min: 8.0, max: 19.0, median: 15.0, gradedCount: 5);
$this->inner->save($evaluationId, $stats);
// Premier appel : va au inner
$result1 = $this->cached->findByEvaluation($evaluationId);
self::assertNotNull($result1);
self::assertSame(14.5, $result1->average);
// Deuxième appel : devrait venir du cache (même résultat)
$result2 = $this->cached->findByEvaluation($evaluationId);
self::assertNotNull($result2);
self::assertSame(14.5, $result2->average);
}
#[Test]
public function itInvalidatesCacheOnSave(): void
{
$evaluationId = EvaluationId::generate();
$stats1 = new ClassStatistics(average: 14.0, min: 8.0, max: 19.0, median: 14.0, gradedCount: 3);
// Sauvegarder et lire pour remplir le cache
$this->cached->save($evaluationId, $stats1);
$this->cached->findByEvaluation($evaluationId);
// Mettre à jour
$stats2 = new ClassStatistics(average: 16.0, min: 10.0, max: 20.0, median: 16.0, gradedCount: 4);
$this->cached->save($evaluationId, $stats2);
$result = $this->cached->findByEvaluation($evaluationId);
self::assertNotNull($result);
self::assertSame(16.0, $result->average);
}
#[Test]
public function itReturnsNullForUnknownEvaluation(): void
{
$result = $this->cached->findByEvaluation(EvaluationId::generate());
self::assertNull($result);
}
#[Test]
public function itInvalidatesCacheOnDelete(): void
{
$evaluationId = EvaluationId::generate();
$stats = new ClassStatistics(average: 14.5, min: 8.0, max: 19.0, median: 15.0, gradedCount: 5);
$this->cached->save($evaluationId, $stats);
// Remplir le cache
$this->cached->findByEvaluation($evaluationId);
// Supprimer
$this->cached->delete($evaluationId);
// Le cache ne doit plus retourner l'ancienne valeur
self::assertNull($this->cached->findByEvaluation($evaluationId));
}
#[Test]
public function itCachesNullResultForUnknownEvaluation(): void
{
$evaluationId = EvaluationId::generate();
// Premier appel : null → mis en cache
self::assertNull($this->cached->findByEvaluation($evaluationId));
// Sauvegarder directement dans inner (sans passer par le cache)
$this->inner->save($evaluationId, new ClassStatistics(
average: 12.0,
min: 10.0,
max: 14.0,
median: 12.0,
gradedCount: 2,
));
// Le cache retourne encore null (valeur cachée)
$result = $this->cached->findByEvaluation($evaluationId);
self::assertNull($result);
}
}