feat: Permettre à l'enseignant de créer et gérer ses évaluations
Les enseignants avaient besoin de définir les critères de notation (barème, coefficient) avant de pouvoir saisir des notes. Sans cette brique, le module Notes & Évaluations (Epic 6) ne pouvait pas démarrer. L'évaluation est un agrégat du bounded context Scolarité avec deux Value Objects (GradeScale 1-100, Coefficient 0.1-10). Le barème est verrouillé dès qu'une note existe pour éviter les incohérences. Un port EvaluationGradesChecker (stub pour l'instant) sera branché sur le repository de notes dans la story 6.2.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Command\CreateEvaluation;
|
||||
|
||||
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\CreateEvaluation\CreateEvaluationCommand;
|
||||
use App\Scolarite\Application\Command\CreateEvaluation\CreateEvaluationHandler;
|
||||
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
|
||||
use App\Scolarite\Domain\Exception\BaremeInvalideException;
|
||||
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
|
||||
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationStatus;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CreateEvaluationHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemoryEvaluationRepository $evaluationRepository;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->evaluationRepository = new InMemoryEvaluationRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-12 10:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesEvaluationSuccessfully(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
$command = $this->createCommand();
|
||||
|
||||
$evaluation = $handler($command);
|
||||
|
||||
self::assertNotEmpty((string) $evaluation->id);
|
||||
self::assertSame(EvaluationStatus::PUBLISHED, $evaluation->status);
|
||||
self::assertSame('Contrôle chapitre 5', $evaluation->title);
|
||||
self::assertSame(20, $evaluation->gradeScale->maxValue);
|
||||
self::assertSame(1.0, $evaluation->coefficient->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itPersistsEvaluationInRepository(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
$command = $this->createCommand();
|
||||
|
||||
$created = $handler($command);
|
||||
|
||||
$evaluation = $this->evaluationRepository->get(
|
||||
EvaluationId::fromString((string) $created->id),
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
);
|
||||
|
||||
self::assertSame('Contrôle chapitre 5', $evaluation->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenTeacherNotAffected(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: false);
|
||||
|
||||
$this->expectException(EnseignantNonAffecteException::class);
|
||||
|
||||
$handler($this->createCommand());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesEvaluationWithCustomGradeScale(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
$command = $this->createCommand(gradeScale: 10);
|
||||
|
||||
$evaluation = $handler($command);
|
||||
|
||||
self::assertSame(10, $evaluation->gradeScale->maxValue);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesEvaluationWithCustomCoefficient(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
$command = $this->createCommand(coefficient: 2.5);
|
||||
|
||||
$evaluation = $handler($command);
|
||||
|
||||
self::assertSame(2.5, $evaluation->coefficient->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenGradeScaleIsInvalid(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$this->expectException(BaremeInvalideException::class);
|
||||
|
||||
$handler($this->createCommand(gradeScale: 0));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenCoefficientIsInvalid(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
|
||||
$this->expectException(CoefficientInvalideException::class);
|
||||
|
||||
$handler($this->createCommand(coefficient: 0.0));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAllowsNullDescription(): void
|
||||
{
|
||||
$handler = $this->createHandler(affecte: true);
|
||||
$command = $this->createCommand(description: null);
|
||||
|
||||
$evaluation = $handler($command);
|
||||
|
||||
self::assertNull($evaluation->description);
|
||||
}
|
||||
|
||||
private function createHandler(bool $affecte): CreateEvaluationHandler
|
||||
{
|
||||
$affectationChecker = new class($affecte) implements EnseignantAffectationChecker {
|
||||
public function __construct(private readonly bool $affecte)
|
||||
{
|
||||
}
|
||||
|
||||
public function estAffecte(UserId $teacherId, ClassId $classId, SubjectId $subjectId, TenantId $tenantId): bool
|
||||
{
|
||||
return $this->affecte;
|
||||
}
|
||||
};
|
||||
|
||||
return new CreateEvaluationHandler(
|
||||
$this->evaluationRepository,
|
||||
$affectationChecker,
|
||||
$this->clock,
|
||||
);
|
||||
}
|
||||
|
||||
private function createCommand(
|
||||
?string $description = 'Évaluation sur les fonctions',
|
||||
int $gradeScale = 20,
|
||||
float $coefficient = 1.0,
|
||||
): CreateEvaluationCommand {
|
||||
return new CreateEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
classId: self::CLASS_ID,
|
||||
subjectId: self::SUBJECT_ID,
|
||||
teacherId: self::TEACHER_ID,
|
||||
title: 'Contrôle chapitre 5',
|
||||
description: $description,
|
||||
evaluationDate: '2026-04-15',
|
||||
gradeScale: $gradeScale,
|
||||
coefficient: $coefficient,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Command\DeleteEvaluation;
|
||||
|
||||
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\DeleteEvaluation\DeleteEvaluationCommand;
|
||||
use App\Scolarite\Application\Command\DeleteEvaluation\DeleteEvaluationHandler;
|
||||
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
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\EvaluationStatus;
|
||||
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class DeleteEvaluationHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
private const string OTHER_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440099';
|
||||
|
||||
private InMemoryEvaluationRepository $evaluationRepository;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->evaluationRepository = new InMemoryEvaluationRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-14 08:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSoftDeletesEvaluation(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$result = $handler(new DeleteEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
));
|
||||
|
||||
self::assertSame(EvaluationStatus::DELETED, $result->status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenTeacherIsNotOwner(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$this->expectException(NonProprietaireDeLEvaluationException::class);
|
||||
|
||||
$handler(new DeleteEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::OTHER_TEACHER_ID,
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenEvaluationNotFound(): void
|
||||
{
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$this->expectException(EvaluationNotFoundException::class);
|
||||
|
||||
$handler(new DeleteEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: '550e8400-e29b-41d4-a716-446655449999',
|
||||
teacherId: self::TEACHER_ID,
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenAlreadyDeleted(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$evaluation->supprimer(new DateTimeImmutable('2026-03-13'));
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
$handler = $this->createHandler();
|
||||
|
||||
$this->expectException(EvaluationDejaSupprimeeException::class);
|
||||
|
||||
$handler(new DeleteEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
));
|
||||
}
|
||||
|
||||
private function createHandler(): DeleteEvaluationHandler
|
||||
{
|
||||
return new DeleteEvaluationHandler(
|
||||
$this->evaluationRepository,
|
||||
$this->clock,
|
||||
);
|
||||
}
|
||||
|
||||
private function createAndSaveEvaluation(): Evaluation
|
||||
{
|
||||
$evaluation = Evaluation::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString(self::CLASS_ID),
|
||||
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
title: 'Contrôle chapitre 5',
|
||||
description: 'Évaluation sur les fonctions',
|
||||
evaluationDate: new DateTimeImmutable('2026-04-15'),
|
||||
gradeScale: new GradeScale(20),
|
||||
coefficient: new Coefficient(1.0),
|
||||
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
||||
);
|
||||
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Command\UpdateEvaluation;
|
||||
|
||||
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\UpdateEvaluation\UpdateEvaluationCommand;
|
||||
use App\Scolarite\Application\Command\UpdateEvaluation\UpdateEvaluationHandler;
|
||||
use App\Scolarite\Application\Port\EvaluationGradesChecker;
|
||||
use App\Scolarite\Domain\Exception\BaremeNonModifiableException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
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\GradeScale;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryEvaluationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UpdateEvaluationHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
private const string OTHER_TEACHER_ID = '550e8400-e29b-41d4-a716-446655440099';
|
||||
|
||||
private InMemoryEvaluationRepository $evaluationRepository;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->evaluationRepository = new InMemoryEvaluationRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-13 14:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUpdatesEvaluationSuccessfully(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$handler = $this->createHandler(hasGrades: false);
|
||||
|
||||
$command = new UpdateEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
title: 'Titre modifié',
|
||||
description: 'Nouvelle description',
|
||||
evaluationDate: '2026-04-20',
|
||||
coefficient: 2.0,
|
||||
);
|
||||
|
||||
$updated = $handler($command);
|
||||
|
||||
self::assertSame('Titre modifié', $updated->title);
|
||||
self::assertSame('Nouvelle description', $updated->description);
|
||||
self::assertSame(2.0, $updated->coefficient->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAllowsGradeScaleChangeWhenNoGrades(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$handler = $this->createHandler(hasGrades: false);
|
||||
|
||||
$command = new UpdateEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
title: $evaluation->title,
|
||||
description: $evaluation->description,
|
||||
evaluationDate: '2026-04-15',
|
||||
gradeScale: 10,
|
||||
);
|
||||
|
||||
$updated = $handler($command);
|
||||
|
||||
self::assertSame(10, $updated->gradeScale->maxValue);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itBlocksGradeScaleChangeWhenGradesExist(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$handler = $this->createHandler(hasGrades: true);
|
||||
|
||||
$this->expectException(BaremeNonModifiableException::class);
|
||||
|
||||
$handler(new UpdateEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
title: $evaluation->title,
|
||||
description: $evaluation->description,
|
||||
evaluationDate: '2026-04-15',
|
||||
gradeScale: 10,
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenTeacherIsNotOwner(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$handler = $this->createHandler(hasGrades: false);
|
||||
|
||||
$this->expectException(NonProprietaireDeLEvaluationException::class);
|
||||
|
||||
$handler(new UpdateEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::OTHER_TEACHER_ID,
|
||||
title: 'Titre',
|
||||
description: null,
|
||||
evaluationDate: '2026-04-15',
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenEvaluationNotFound(): void
|
||||
{
|
||||
$handler = $this->createHandler(hasGrades: false);
|
||||
|
||||
$this->expectException(EvaluationNotFoundException::class);
|
||||
|
||||
$handler(new UpdateEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: '550e8400-e29b-41d4-a716-446655449999',
|
||||
teacherId: self::TEACHER_ID,
|
||||
title: 'Titre',
|
||||
description: null,
|
||||
evaluationDate: '2026-04-15',
|
||||
));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsWhenEvaluationIsDeleted(): void
|
||||
{
|
||||
$evaluation = $this->createAndSaveEvaluation();
|
||||
$evaluation->supprimer(new DateTimeImmutable('2026-03-13'));
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
$handler = $this->createHandler(hasGrades: false);
|
||||
|
||||
$this->expectException(EvaluationDejaSupprimeeException::class);
|
||||
|
||||
$handler(new UpdateEvaluationCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
evaluationId: (string) $evaluation->id,
|
||||
teacherId: self::TEACHER_ID,
|
||||
title: 'Titre',
|
||||
description: null,
|
||||
evaluationDate: '2026-04-15',
|
||||
));
|
||||
}
|
||||
|
||||
private function createHandler(bool $hasGrades): UpdateEvaluationHandler
|
||||
{
|
||||
$gradesChecker = new class($hasGrades) implements EvaluationGradesChecker {
|
||||
public function __construct(private readonly bool $hasGrades)
|
||||
{
|
||||
}
|
||||
|
||||
public function hasGrades(EvaluationId $evaluationId, TenantId $tenantId): bool
|
||||
{
|
||||
return $this->hasGrades;
|
||||
}
|
||||
};
|
||||
|
||||
return new UpdateEvaluationHandler(
|
||||
$this->evaluationRepository,
|
||||
$gradesChecker,
|
||||
$this->clock,
|
||||
);
|
||||
}
|
||||
|
||||
private function createAndSaveEvaluation(): Evaluation
|
||||
{
|
||||
$evaluation = Evaluation::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString(self::CLASS_ID),
|
||||
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
title: 'Contrôle chapitre 5',
|
||||
description: 'Évaluation sur les fonctions',
|
||||
evaluationDate: new DateTimeImmutable('2026-04-15'),
|
||||
gradeScale: new GradeScale(20),
|
||||
coefficient: new Coefficient(1.0),
|
||||
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
||||
);
|
||||
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user