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:
37
backend/src/Scolarite/Domain/Event/EvaluationCreee.php
Normal file
37
backend/src/Scolarite/Domain/Event/EvaluationCreee.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EvaluationCreee implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EvaluationId $evaluationId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $teacherId,
|
||||
public string $title,
|
||||
public DateTimeImmutable $evaluationDate,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->evaluationId->value;
|
||||
}
|
||||
}
|
||||
34
backend/src/Scolarite/Domain/Event/EvaluationModifiee.php
Normal file
34
backend/src/Scolarite/Domain/Event/EvaluationModifiee.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EvaluationModifiee implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EvaluationId $evaluationId,
|
||||
public string $title,
|
||||
public DateTimeImmutable $evaluationDate,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->evaluationId->value;
|
||||
}
|
||||
}
|
||||
32
backend/src/Scolarite/Domain/Event/EvaluationSupprimee.php
Normal file
32
backend/src/Scolarite/Domain/Event/EvaluationSupprimee.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EvaluationSupprimee implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EvaluationId $evaluationId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->evaluationId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class BaremeInvalideException extends DomainException
|
||||
{
|
||||
public static function avecValeur(int $maxValue): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le barème doit être compris entre 1 et 100, %d donné.',
|
||||
$maxValue,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class BaremeNonModifiableException extends DomainException
|
||||
{
|
||||
public static function carNotesExistantes(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le barème de l\'évaluation "%s" ne peut pas être modifié car des notes ont déjà été saisies.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class CoefficientInvalideException extends DomainException
|
||||
{
|
||||
public static function avecValeur(float $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le coefficient doit être compris entre 0.1 et 10, %s donné.',
|
||||
$value,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EvaluationDejaSupprimeeException extends DomainException
|
||||
{
|
||||
public static function withId(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'évaluation avec l\'ID "%s" est déjà supprimée.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EvaluationNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'évaluation avec l\'ID "%s" n\'a pas été trouvée.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class NonProprietaireDeLEvaluationException extends DomainException
|
||||
{
|
||||
public static function withId(EvaluationId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Vous n\'êtes pas le propriétaire de l\'évaluation "%s".',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Scolarite\Domain\Exception\CoefficientInvalideException;
|
||||
|
||||
final readonly class Coefficient
|
||||
{
|
||||
public function __construct(
|
||||
public float $value,
|
||||
) {
|
||||
if ($value < 0.1 || $value > 10) {
|
||||
throw CoefficientInvalideException::avecValeur($value);
|
||||
}
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
}
|
||||
168
backend/src/Scolarite/Domain/Model/Evaluation/Evaluation.php
Normal file
168
backend/src/Scolarite/Domain/Model/Evaluation/Evaluation.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Event\EvaluationCreee;
|
||||
use App\Scolarite\Domain\Event\EvaluationModifiee;
|
||||
use App\Scolarite\Domain\Event\EvaluationSupprimee;
|
||||
use App\Scolarite\Domain\Exception\BaremeNonModifiableException;
|
||||
use App\Scolarite\Domain\Exception\EvaluationDejaSupprimeeException;
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class Evaluation extends AggregateRoot
|
||||
{
|
||||
public private(set) DateTimeImmutable $updatedAt;
|
||||
|
||||
private function __construct(
|
||||
public private(set) EvaluationId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) ClassId $classId,
|
||||
public private(set) SubjectId $subjectId,
|
||||
public private(set) UserId $teacherId,
|
||||
public private(set) string $title,
|
||||
public private(set) ?string $description,
|
||||
public private(set) DateTimeImmutable $evaluationDate,
|
||||
public private(set) GradeScale $gradeScale,
|
||||
public private(set) Coefficient $coefficient,
|
||||
public private(set) EvaluationStatus $status,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
$this->updatedAt = $createdAt;
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
TenantId $tenantId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
UserId $teacherId,
|
||||
string $title,
|
||||
?string $description,
|
||||
DateTimeImmutable $evaluationDate,
|
||||
GradeScale $gradeScale,
|
||||
Coefficient $coefficient,
|
||||
DateTimeImmutable $now,
|
||||
): self {
|
||||
$evaluation = new self(
|
||||
id: EvaluationId::generate(),
|
||||
tenantId: $tenantId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
teacherId: $teacherId,
|
||||
title: $title,
|
||||
description: $description,
|
||||
evaluationDate: $evaluationDate,
|
||||
gradeScale: $gradeScale,
|
||||
coefficient: $coefficient,
|
||||
status: EvaluationStatus::PUBLISHED,
|
||||
createdAt: $now,
|
||||
);
|
||||
|
||||
$evaluation->recordEvent(new EvaluationCreee(
|
||||
evaluationId: $evaluation->id,
|
||||
classId: (string) $classId,
|
||||
subjectId: (string) $subjectId,
|
||||
teacherId: (string) $teacherId,
|
||||
title: $title,
|
||||
evaluationDate: $evaluationDate,
|
||||
occurredOn: $now,
|
||||
));
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
|
||||
public function modifier(
|
||||
string $title,
|
||||
?string $description,
|
||||
Coefficient $coefficient,
|
||||
DateTimeImmutable $evaluationDate,
|
||||
?GradeScale $gradeScale,
|
||||
bool $hasGrades,
|
||||
DateTimeImmutable $now,
|
||||
): void {
|
||||
if ($this->status === EvaluationStatus::DELETED) {
|
||||
throw EvaluationDejaSupprimeeException::withId($this->id);
|
||||
}
|
||||
|
||||
if ($gradeScale !== null && !$this->gradeScale->equals($gradeScale) && $hasGrades) {
|
||||
throw BaremeNonModifiableException::carNotesExistantes($this->id);
|
||||
}
|
||||
|
||||
$this->title = $title;
|
||||
$this->description = $description;
|
||||
$this->coefficient = $coefficient;
|
||||
$this->evaluationDate = $evaluationDate;
|
||||
|
||||
if ($gradeScale !== null && !$hasGrades) {
|
||||
$this->gradeScale = $gradeScale;
|
||||
}
|
||||
|
||||
$this->updatedAt = $now;
|
||||
|
||||
$this->recordEvent(new EvaluationModifiee(
|
||||
evaluationId: $this->id,
|
||||
title: $title,
|
||||
evaluationDate: $evaluationDate,
|
||||
occurredOn: $now,
|
||||
));
|
||||
}
|
||||
|
||||
public function supprimer(DateTimeImmutable $now): void
|
||||
{
|
||||
if ($this->status === EvaluationStatus::DELETED) {
|
||||
throw EvaluationDejaSupprimeeException::withId($this->id);
|
||||
}
|
||||
|
||||
$this->status = EvaluationStatus::DELETED;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
$this->recordEvent(new EvaluationSupprimee(
|
||||
evaluationId: $this->id,
|
||||
occurredOn: $now,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
EvaluationId $id,
|
||||
TenantId $tenantId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
UserId $teacherId,
|
||||
string $title,
|
||||
?string $description,
|
||||
DateTimeImmutable $evaluationDate,
|
||||
GradeScale $gradeScale,
|
||||
Coefficient $coefficient,
|
||||
EvaluationStatus $status,
|
||||
DateTimeImmutable $createdAt,
|
||||
DateTimeImmutable $updatedAt,
|
||||
): self {
|
||||
$evaluation = new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
teacherId: $teacherId,
|
||||
title: $title,
|
||||
description: $description,
|
||||
evaluationDate: $evaluationDate,
|
||||
gradeScale: $gradeScale,
|
||||
coefficient: $coefficient,
|
||||
status: $status,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$evaluation->updatedAt = $updatedAt;
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class EvaluationId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
enum EvaluationStatus: string
|
||||
{
|
||||
case PUBLISHED = 'published';
|
||||
case DELETED = 'deleted';
|
||||
}
|
||||
30
backend/src/Scolarite/Domain/Model/Evaluation/GradeScale.php
Normal file
30
backend/src/Scolarite/Domain/Model/Evaluation/GradeScale.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
use App\Scolarite\Domain\Exception\BaremeInvalideException;
|
||||
|
||||
use function round;
|
||||
|
||||
final readonly class GradeScale
|
||||
{
|
||||
public function __construct(
|
||||
public int $maxValue,
|
||||
) {
|
||||
if ($maxValue < 1 || $maxValue > 100) {
|
||||
throw BaremeInvalideException::avecValeur($maxValue);
|
||||
}
|
||||
}
|
||||
|
||||
public function convertTo20(float $grade): float
|
||||
{
|
||||
return round(($grade / $this->maxValue) * 20, 2);
|
||||
}
|
||||
|
||||
public function equals(self $other): bool
|
||||
{
|
||||
return $this->maxValue === $other->maxValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\EvaluationNotFoundException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface EvaluationRepository
|
||||
{
|
||||
public function save(Evaluation $evaluation): void;
|
||||
|
||||
/** @throws EvaluationNotFoundException */
|
||||
public function get(EvaluationId $id, TenantId $tenantId): Evaluation;
|
||||
|
||||
public function findById(EvaluationId $id, TenantId $tenantId): ?Evaluation;
|
||||
|
||||
/** @return array<Evaluation> */
|
||||
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array;
|
||||
|
||||
/** @return array<Evaluation> */
|
||||
public function findByTeacherAndClass(UserId $teacherId, ClassId $classId, TenantId $tenantId): array;
|
||||
|
||||
/** @return array<Evaluation> */
|
||||
public function findByClass(ClassId $classId, TenantId $tenantId): array;
|
||||
}
|
||||
Reference in New Issue
Block a user