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 e745cf326a
733 changed files with 113156 additions and 286 deletions

View File

@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Infrastructure\Cache;
use App\Administration\Domain\Model\Subject\SubjectId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Infrastructure\Cache\CachingStudentAverageRepository;
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryStudentAverageRepository;
use App\Shared\Domain\Tenant\TenantId;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
final class CachingStudentAverageRepositoryTest extends TestCase
{
private const string TENANT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
private const string STUDENT_ID = '22222222-2222-2222-2222-222222222222';
private const string SUBJECT_ID = '66666666-6666-6666-6666-666666666666';
private const string PERIOD_ID = '11111111-1111-1111-1111-111111111111';
private InMemoryStudentAverageRepository $inner;
private CachingStudentAverageRepository $cached;
protected function setUp(): void
{
$this->inner = new InMemoryStudentAverageRepository();
$this->cached = new CachingStudentAverageRepository(
inner: $this->inner,
cache: new ArrayAdapter(),
);
}
#[Test]
public function itCachesSubjectAveragesOnRead(): void
{
$tenantId = TenantId::fromString(self::TENANT_ID);
$studentId = UserId::fromString(self::STUDENT_ID);
$this->inner->saveSubjectAverage(
$tenantId,
$studentId,
SubjectId::fromString(self::SUBJECT_ID),
self::PERIOD_ID,
15.0,
3,
);
$result1 = $this->cached->findSubjectAveragesForStudent($studentId, self::PERIOD_ID, $tenantId);
$result2 = $this->cached->findSubjectAveragesForStudent($studentId, self::PERIOD_ID, $tenantId);
self::assertSame([15.0], $result1);
self::assertSame([15.0], $result2);
}
#[Test]
public function itInvalidatesCacheOnSaveSubjectAverage(): void
{
$tenantId = TenantId::fromString(self::TENANT_ID);
$studentId = UserId::fromString(self::STUDENT_ID);
$subjectId = SubjectId::fromString(self::SUBJECT_ID);
$this->cached->saveSubjectAverage($tenantId, $studentId, $subjectId, self::PERIOD_ID, 14.0, 2);
// Remplir le cache
$this->cached->findSubjectAveragesForStudent($studentId, self::PERIOD_ID, $tenantId);
// Mettre à jour → doit invalider le cache
$this->cached->saveSubjectAverage($tenantId, $studentId, $subjectId, self::PERIOD_ID, 16.0, 3);
$result = $this->cached->findSubjectAveragesForStudent($studentId, self::PERIOD_ID, $tenantId);
self::assertSame([16.0], $result);
}
#[Test]
public function itReturnsEmptyArrayWhenNoAverages(): void
{
$result = $this->cached->findSubjectAveragesForStudent(
UserId::fromString(self::STUDENT_ID),
self::PERIOD_ID,
TenantId::fromString(self::TENANT_ID),
);
self::assertSame([], $result);
}
#[Test]
public function itDelegatesToInnerForGeneralAverage(): void
{
$tenantId = TenantId::fromString(self::TENANT_ID);
$studentId = UserId::fromString(self::STUDENT_ID);
$this->cached->saveGeneralAverage($tenantId, $studentId, self::PERIOD_ID, 13.5);
$result = $this->cached->findGeneralAverageForStudent($studentId, self::PERIOD_ID, $tenantId);
self::assertSame(13.5, $result);
}
#[Test]
public function itInvalidatesCacheOnDeleteSubjectAverage(): void
{
$tenantId = TenantId::fromString(self::TENANT_ID);
$studentId = UserId::fromString(self::STUDENT_ID);
$subjectId = SubjectId::fromString(self::SUBJECT_ID);
$this->cached->saveSubjectAverage($tenantId, $studentId, $subjectId, self::PERIOD_ID, 14.0, 2);
// Remplir le cache
$this->cached->findSubjectAveragesForStudent($studentId, self::PERIOD_ID, $tenantId);
// Supprimer → doit invalider le cache
$this->cached->deleteSubjectAverage($studentId, $subjectId, self::PERIOD_ID, $tenantId);
$result = $this->cached->findSubjectAveragesForStudent($studentId, self::PERIOD_ID, $tenantId);
self::assertSame([], $result);
}
#[Test]
public function itCachesMultipleSubjectAverages(): void
{
$tenantId = TenantId::fromString(self::TENANT_ID);
$studentId = UserId::fromString(self::STUDENT_ID);
$subject2Id = '77777777-7777-7777-7777-777777777777';
$this->cached->saveSubjectAverage(
$tenantId,
$studentId,
SubjectId::fromString(self::SUBJECT_ID),
self::PERIOD_ID,
15.0,
3,
);
$this->cached->saveSubjectAverage(
$tenantId,
$studentId,
SubjectId::fromString($subject2Id),
self::PERIOD_ID,
12.0,
2,
);
$result = $this->cached->findSubjectAveragesForStudent($studentId, self::PERIOD_ID, $tenantId);
self::assertCount(2, $result);
self::assertContains(15.0, $result);
self::assertContains(12.0, $result);
}
}