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.
188 lines
6.4 KiB
PHP
188 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Application\Command\SaveAppreciation;
|
|
|
|
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
|
use App\Administration\Domain\Model\Subject\SubjectId;
|
|
use App\Administration\Domain\Model\User\UserId;
|
|
use App\Scolarite\Application\Command\SaveAppreciation\SaveAppreciationCommand;
|
|
use App\Scolarite\Application\Command\SaveAppreciation\SaveAppreciationHandler;
|
|
use App\Scolarite\Domain\Exception\AppreciationTropLongueException;
|
|
use App\Scolarite\Domain\Exception\GradeNotFoundException;
|
|
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
|
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
|
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
|
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
|
|
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
|
use App\Scolarite\Domain\Model\Grade\Grade;
|
|
use App\Scolarite\Domain\Model\Grade\GradeStatus;
|
|
use App\Scolarite\Domain\Model\Grade\GradeValue;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationRepository;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryGradeRepository;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
use function str_repeat;
|
|
|
|
final class SaveAppreciationHandlerTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string EVALUATION_ID = '550e8400-e29b-41d4-a716-446655440040';
|
|
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
|
private const string STUDENT_ID = '550e8400-e29b-41d4-a716-446655440050';
|
|
|
|
private InMemoryEvaluationRepository $evaluationRepository;
|
|
private InMemoryGradeRepository $gradeRepository;
|
|
private Clock $clock;
|
|
private string $gradeId;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->evaluationRepository = new InMemoryEvaluationRepository();
|
|
$this->gradeRepository = new InMemoryGradeRepository();
|
|
$this->clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-03-31 10:00:00');
|
|
}
|
|
};
|
|
|
|
$this->seedEvaluationAndGrade();
|
|
}
|
|
|
|
#[Test]
|
|
public function itSavesAppreciation(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$grade = $handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: 'Très bon travail',
|
|
));
|
|
|
|
self::assertSame('Très bon travail', $grade->appreciation);
|
|
self::assertNotNull($grade->appreciationUpdatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function itClearsAppreciation(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: 'Bon travail',
|
|
));
|
|
|
|
$grade = $handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: null,
|
|
));
|
|
|
|
self::assertNull($grade->appreciation);
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenTeacherNotOwner(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$this->expectException(NonProprietaireDeLEvaluationException::class);
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: '550e8400-e29b-41d4-a716-446655440099',
|
|
appreciation: 'Test',
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenGradeNotFound(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$this->expectException(GradeNotFoundException::class);
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: '550e8400-e29b-41d4-a716-446655440099',
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: 'Test',
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenAppreciationTooLong(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$this->expectException(AppreciationTropLongueException::class);
|
|
|
|
$handler(new SaveAppreciationCommand(
|
|
tenantId: self::TENANT_ID,
|
|
gradeId: $this->gradeId,
|
|
teacherId: self::TEACHER_ID,
|
|
appreciation: str_repeat('a', 501),
|
|
));
|
|
}
|
|
|
|
private function createHandler(): SaveAppreciationHandler
|
|
{
|
|
return new SaveAppreciationHandler(
|
|
$this->evaluationRepository,
|
|
$this->gradeRepository,
|
|
$this->clock,
|
|
);
|
|
}
|
|
|
|
private function seedEvaluationAndGrade(): void
|
|
{
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
|
|
$evaluation = Evaluation::reconstitute(
|
|
id: EvaluationId::fromString(self::EVALUATION_ID),
|
|
tenantId: $tenantId,
|
|
classId: ClassId::fromString('550e8400-e29b-41d4-a716-446655440020'),
|
|
subjectId: SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
title: 'Contrôle',
|
|
description: null,
|
|
evaluationDate: new DateTimeImmutable('2026-04-15'),
|
|
gradeScale: new GradeScale(20),
|
|
coefficient: new Coefficient(1.0),
|
|
status: EvaluationStatus::PUBLISHED,
|
|
createdAt: new DateTimeImmutable('2026-03-12 10:00:00'),
|
|
updatedAt: new DateTimeImmutable('2026-03-12 10:00:00'),
|
|
);
|
|
|
|
$this->evaluationRepository->save($evaluation);
|
|
|
|
$grade = Grade::saisir(
|
|
tenantId: $tenantId,
|
|
evaluationId: EvaluationId::fromString(self::EVALUATION_ID),
|
|
studentId: UserId::fromString(self::STUDENT_ID),
|
|
value: new GradeValue(15.5),
|
|
status: GradeStatus::GRADED,
|
|
gradeScale: new GradeScale(20),
|
|
createdBy: UserId::fromString(self::TEACHER_ID),
|
|
now: new DateTimeImmutable('2026-03-27 10:00:00'),
|
|
);
|
|
|
|
$this->gradeRepository->save($grade);
|
|
$this->gradeId = (string) $grade->id;
|
|
}
|
|
}
|