feat: Calculer automatiquement les moyennes après chaque saisie de notes
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user