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,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Competency\StudentCompetencyResultId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class ResultatCompetenceModifie implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public StudentCompetencyResultId $resultId,
|
||||
public string $competencyEvaluationId,
|
||||
public string $studentId,
|
||||
public string $oldLevelCode,
|
||||
public string $newLevelCode,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->resultId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Event;
|
||||
|
||||
use App\Scolarite\Domain\Model\Competency\StudentCompetencyResultId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class ResultatCompetenceSaisi implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public StudentCompetencyResultId $resultId,
|
||||
public string $competencyEvaluationId,
|
||||
public string $studentId,
|
||||
public string $levelCode,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->resultId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Grade\AppreciationTemplateId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class AppreciationTemplateNonTrouveeException extends DomainException
|
||||
{
|
||||
public static function withId(AppreciationTemplateId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le modèle d\'appréciation avec l\'ID "%s" n\'a pas été trouvé.',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class AppreciationTropLongueException extends DomainException
|
||||
{
|
||||
public static function depasseLimite(int $length, int $maxLength): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'L\'appréciation dépasse la limite de %d caractères (%d donnés).',
|
||||
$maxLength,
|
||||
$length,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class CategorieAppreciationInvalideException extends DomainException
|
||||
{
|
||||
public static function pourValeur(string $value): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'La catégorie d\'appréciation "%s" n\'est pas valide. Valeurs acceptées : positive, neutral, improvement.',
|
||||
$value,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -18,4 +18,13 @@ final class GradeNotFoundException extends DomainException
|
||||
$id,
|
||||
));
|
||||
}
|
||||
|
||||
public static function forStudent(string $studentId, string $evaluationId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Aucune note trouvée pour l\'élève "%s" dans l\'évaluation "%s".',
|
||||
$studentId,
|
||||
$evaluationId,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Exception;
|
||||
|
||||
use App\Scolarite\Domain\Model\Grade\AppreciationTemplateId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class NonProprietaireDuModeleException extends DomainException
|
||||
{
|
||||
public static function withId(AppreciationTemplateId $id): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Vous n\'êtes pas le propriétaire du modèle d\'appréciation "%s".',
|
||||
$id,
|
||||
));
|
||||
}
|
||||
}
|
||||
61
backend/src/Scolarite/Domain/Model/Competency/Competency.php
Normal file
61
backend/src/Scolarite/Domain/Model/Competency/Competency.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
final class Competency
|
||||
{
|
||||
private function __construct(
|
||||
public private(set) CompetencyId $id,
|
||||
public private(set) CompetencyFrameworkId $frameworkId,
|
||||
public private(set) string $code,
|
||||
public private(set) string $name,
|
||||
public private(set) ?string $description,
|
||||
public private(set) ?CompetencyId $parentId,
|
||||
public private(set) int $sortOrder,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
CompetencyFrameworkId $frameworkId,
|
||||
string $code,
|
||||
string $name,
|
||||
?string $description,
|
||||
?CompetencyId $parentId,
|
||||
int $sortOrder,
|
||||
): self {
|
||||
return new self(
|
||||
id: CompetencyId::generate(),
|
||||
frameworkId: $frameworkId,
|
||||
code: $code,
|
||||
name: $name,
|
||||
description: $description,
|
||||
parentId: $parentId,
|
||||
sortOrder: $sortOrder,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
CompetencyId $id,
|
||||
CompetencyFrameworkId $frameworkId,
|
||||
string $code,
|
||||
string $name,
|
||||
?string $description,
|
||||
?CompetencyId $parentId,
|
||||
int $sortOrder,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
frameworkId: $frameworkId,
|
||||
code: $code,
|
||||
name: $name,
|
||||
description: $description,
|
||||
parentId: $parentId,
|
||||
sortOrder: $sortOrder,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
|
||||
final class CompetencyEvaluation
|
||||
{
|
||||
private function __construct(
|
||||
public private(set) CompetencyEvaluationId $id,
|
||||
public private(set) EvaluationId $evaluationId,
|
||||
public private(set) CompetencyId $competencyId,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
EvaluationId $evaluationId,
|
||||
CompetencyId $competencyId,
|
||||
): self {
|
||||
return new self(
|
||||
id: CompetencyEvaluationId::generate(),
|
||||
evaluationId: $evaluationId,
|
||||
competencyId: $competencyId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
CompetencyEvaluationId $id,
|
||||
EvaluationId $evaluationId,
|
||||
CompetencyId $competencyId,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
evaluationId: $evaluationId,
|
||||
competencyId: $competencyId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class CompetencyEvaluationId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class CompetencyFramework
|
||||
{
|
||||
private function __construct(
|
||||
public private(set) CompetencyFrameworkId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) string $name,
|
||||
public private(set) bool $isDefault,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
TenantId $tenantId,
|
||||
string $name,
|
||||
bool $isDefault,
|
||||
DateTimeImmutable $now,
|
||||
): self {
|
||||
return new self(
|
||||
id: CompetencyFrameworkId::generate(),
|
||||
tenantId: $tenantId,
|
||||
name: $name,
|
||||
isDefault: $isDefault,
|
||||
createdAt: $now,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
CompetencyFrameworkId $id,
|
||||
TenantId $tenantId,
|
||||
string $name,
|
||||
bool $isDefault,
|
||||
DateTimeImmutable $createdAt,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
name: $name,
|
||||
isDefault: $isDefault,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class CompetencyFrameworkId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class CompetencyId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
/**
|
||||
* Niveaux de compétence standards du référentiel français.
|
||||
*
|
||||
* Ces niveaux sont les valeurs par défaut pour tout établissement
|
||||
* en mode compétences. Un établissement peut définir des niveaux
|
||||
* personnalisés dans la table competency_levels.
|
||||
*
|
||||
* @see FR23 : Mode compétences sans notes chiffrées
|
||||
*/
|
||||
enum CompetencyLevel: string
|
||||
{
|
||||
case NOT_ACQUIRED = 'not_acquired';
|
||||
case IN_PROGRESS = 'in_progress';
|
||||
case ACQUIRED = 'acquired';
|
||||
case EXCEEDED = 'exceeded';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NOT_ACQUIRED => 'Non acquis',
|
||||
self::IN_PROGRESS => 'En cours d\'acquisition',
|
||||
self::ACQUIRED => 'Acquis',
|
||||
self::EXCEEDED => 'Dépassé',
|
||||
};
|
||||
}
|
||||
|
||||
public function sortOrder(): int
|
||||
{
|
||||
return match ($this) {
|
||||
self::NOT_ACQUIRED => 1,
|
||||
self::IN_PROGRESS => 2,
|
||||
self::ACQUIRED => 3,
|
||||
self::EXCEEDED => 4,
|
||||
};
|
||||
}
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NOT_ACQUIRED => '#e74c3c',
|
||||
self::IN_PROGRESS => '#f39c12',
|
||||
self::ACQUIRED => '#2ecc71',
|
||||
self::EXCEEDED => '#3498db',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DomainException;
|
||||
|
||||
use function preg_match;
|
||||
|
||||
/**
|
||||
* Niveau de compétence personnalisé par établissement.
|
||||
*
|
||||
* Permet à chaque établissement de définir ses propres niveaux
|
||||
* au lieu d'utiliser les niveaux standards (CompetencyLevel enum).
|
||||
*/
|
||||
final class CustomCompetencyLevel
|
||||
{
|
||||
private function __construct(
|
||||
public private(set) CustomCompetencyLevelId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) string $code,
|
||||
public private(set) string $name,
|
||||
public private(set) ?string $color,
|
||||
public private(set) int $sortOrder,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
TenantId $tenantId,
|
||||
string $code,
|
||||
string $name,
|
||||
?string $color,
|
||||
int $sortOrder,
|
||||
): self {
|
||||
if ($color !== null && preg_match('/^#[0-9a-fA-F]{6}$/', $color) !== 1) {
|
||||
throw new DomainException('La couleur doit être au format hexadécimal (#rrggbb).');
|
||||
}
|
||||
|
||||
return new self(
|
||||
id: CustomCompetencyLevelId::generate(),
|
||||
tenantId: $tenantId,
|
||||
code: $code,
|
||||
name: $name,
|
||||
color: $color,
|
||||
sortOrder: $sortOrder,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
CustomCompetencyLevelId $id,
|
||||
TenantId $tenantId,
|
||||
string $code,
|
||||
string $name,
|
||||
?string $color,
|
||||
int $sortOrder,
|
||||
): self {
|
||||
return new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
code: $code,
|
||||
name: $name,
|
||||
color: $color,
|
||||
sortOrder: $sortOrder,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class CustomCompetencyLevelId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Event\ResultatCompetenceModifie;
|
||||
use App\Scolarite\Domain\Event\ResultatCompetenceSaisi;
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class StudentCompetencyResult extends AggregateRoot
|
||||
{
|
||||
public private(set) DateTimeImmutable $updatedAt;
|
||||
|
||||
private function __construct(
|
||||
public private(set) StudentCompetencyResultId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) CompetencyEvaluationId $competencyEvaluationId,
|
||||
public private(set) UserId $studentId,
|
||||
public private(set) string $levelCode,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
$this->updatedAt = $createdAt;
|
||||
}
|
||||
|
||||
public static function saisir(
|
||||
TenantId $tenantId,
|
||||
CompetencyEvaluationId $competencyEvaluationId,
|
||||
UserId $studentId,
|
||||
string $levelCode,
|
||||
DateTimeImmutable $now,
|
||||
): self {
|
||||
$result = new self(
|
||||
id: StudentCompetencyResultId::generate(),
|
||||
tenantId: $tenantId,
|
||||
competencyEvaluationId: $competencyEvaluationId,
|
||||
studentId: $studentId,
|
||||
levelCode: $levelCode,
|
||||
createdAt: $now,
|
||||
);
|
||||
|
||||
$result->recordEvent(new ResultatCompetenceSaisi(
|
||||
resultId: $result->id,
|
||||
competencyEvaluationId: (string) $competencyEvaluationId,
|
||||
studentId: (string) $studentId,
|
||||
levelCode: $levelCode,
|
||||
occurredOn: $now,
|
||||
));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function modifier(
|
||||
string $levelCode,
|
||||
DateTimeImmutable $now,
|
||||
): void {
|
||||
if ($this->levelCode === $levelCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oldLevelCode = $this->levelCode;
|
||||
|
||||
$this->levelCode = $levelCode;
|
||||
$this->updatedAt = $now;
|
||||
|
||||
$this->recordEvent(new ResultatCompetenceModifie(
|
||||
resultId: $this->id,
|
||||
competencyEvaluationId: (string) $this->competencyEvaluationId,
|
||||
studentId: (string) $this->studentId,
|
||||
oldLevelCode: $oldLevelCode,
|
||||
newLevelCode: $levelCode,
|
||||
occurredOn: $now,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
StudentCompetencyResultId $id,
|
||||
TenantId $tenantId,
|
||||
CompetencyEvaluationId $competencyEvaluationId,
|
||||
UserId $studentId,
|
||||
string $levelCode,
|
||||
DateTimeImmutable $createdAt,
|
||||
DateTimeImmutable $updatedAt,
|
||||
): self {
|
||||
$result = new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
competencyEvaluationId: $competencyEvaluationId,
|
||||
studentId: $studentId,
|
||||
levelCode: $levelCode,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$result->updatedAt = $updatedAt;
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Competency;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class StudentCompetencyResultId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Evaluation;
|
||||
|
||||
final readonly class ClassStatistics
|
||||
{
|
||||
public function __construct(
|
||||
public ?float $average,
|
||||
public ?float $min,
|
||||
public ?float $max,
|
||||
public ?float $median,
|
||||
public int $gradedCount,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Grade;
|
||||
|
||||
enum AppreciationCategory: string
|
||||
{
|
||||
case POSITIVE = 'positive';
|
||||
case NEUTRAL = 'neutral';
|
||||
case IMPROVEMENT = 'improvement';
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Grade;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final class AppreciationTemplate
|
||||
{
|
||||
public private(set) DateTimeImmutable $updatedAt;
|
||||
public private(set) int $usageCount;
|
||||
|
||||
private function __construct(
|
||||
public private(set) AppreciationTemplateId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) UserId $teacherId,
|
||||
public private(set) string $title,
|
||||
public private(set) string $content,
|
||||
public private(set) ?AppreciationCategory $category,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
$this->updatedAt = $createdAt;
|
||||
$this->usageCount = 0;
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
TenantId $tenantId,
|
||||
UserId $teacherId,
|
||||
string $title,
|
||||
string $content,
|
||||
?AppreciationCategory $category,
|
||||
DateTimeImmutable $now,
|
||||
): self {
|
||||
return new self(
|
||||
id: AppreciationTemplateId::generate(),
|
||||
tenantId: $tenantId,
|
||||
teacherId: $teacherId,
|
||||
title: $title,
|
||||
content: $content,
|
||||
category: $category,
|
||||
createdAt: $now,
|
||||
);
|
||||
}
|
||||
|
||||
public function modifier(
|
||||
string $title,
|
||||
string $content,
|
||||
?AppreciationCategory $category,
|
||||
DateTimeImmutable $now,
|
||||
): void {
|
||||
$this->title = $title;
|
||||
$this->content = $content;
|
||||
$this->category = $category;
|
||||
$this->updatedAt = $now;
|
||||
}
|
||||
|
||||
public function incrementerUtilisation(): void
|
||||
{
|
||||
++$this->usageCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
public static function reconstitute(
|
||||
AppreciationTemplateId $id,
|
||||
TenantId $tenantId,
|
||||
UserId $teacherId,
|
||||
string $title,
|
||||
string $content,
|
||||
?AppreciationCategory $category,
|
||||
int $usageCount,
|
||||
DateTimeImmutable $createdAt,
|
||||
DateTimeImmutable $updatedAt,
|
||||
): self {
|
||||
$template = new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
teacherId: $teacherId,
|
||||
title: $title,
|
||||
content: $content,
|
||||
category: $category,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$template->updatedAt = $updatedAt;
|
||||
$template->usageCount = $usageCount;
|
||||
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Model\Grade;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class AppreciationTemplateId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace App\Scolarite\Domain\Model\Grade;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Event\NoteModifiee;
|
||||
use App\Scolarite\Domain\Event\NoteSaisie;
|
||||
use App\Scolarite\Domain\Exception\AppreciationTropLongueException;
|
||||
use App\Scolarite\Domain\Exception\NoteRequiseException;
|
||||
use App\Scolarite\Domain\Exception\ValeurNoteInvalideException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
@@ -15,9 +16,15 @@ use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function mb_strlen;
|
||||
|
||||
final class Grade extends AggregateRoot
|
||||
{
|
||||
public const int MAX_APPRECIATION_LENGTH = 500;
|
||||
|
||||
public private(set) DateTimeImmutable $updatedAt;
|
||||
public private(set) ?string $appreciation;
|
||||
public private(set) ?DateTimeImmutable $appreciationUpdatedAt;
|
||||
|
||||
private function __construct(
|
||||
public private(set) GradeId $id,
|
||||
@@ -30,6 +37,8 @@ final class Grade extends AggregateRoot
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
$this->updatedAt = $createdAt;
|
||||
$this->appreciation = null;
|
||||
$this->appreciationUpdatedAt = null;
|
||||
}
|
||||
|
||||
public static function saisir(
|
||||
@@ -96,6 +105,17 @@ final class Grade extends AggregateRoot
|
||||
));
|
||||
}
|
||||
|
||||
public function saisirAppreciation(?string $appreciation, DateTimeImmutable $now): void
|
||||
{
|
||||
if ($appreciation !== null && mb_strlen($appreciation) > self::MAX_APPRECIATION_LENGTH) {
|
||||
throw AppreciationTropLongueException::depasseLimite(mb_strlen($appreciation), self::MAX_APPRECIATION_LENGTH);
|
||||
}
|
||||
|
||||
$this->appreciation = $appreciation !== null && $appreciation !== '' ? $appreciation : null;
|
||||
$this->appreciationUpdatedAt = $now;
|
||||
$this->updatedAt = $now;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Pour usage Infrastructure uniquement
|
||||
*/
|
||||
@@ -109,6 +129,8 @@ final class Grade extends AggregateRoot
|
||||
UserId $createdBy,
|
||||
DateTimeImmutable $createdAt,
|
||||
DateTimeImmutable $updatedAt,
|
||||
?string $appreciation = null,
|
||||
?DateTimeImmutable $appreciationUpdatedAt = null,
|
||||
): self {
|
||||
$grade = new self(
|
||||
id: $id,
|
||||
@@ -122,6 +144,8 @@ final class Grade extends AggregateRoot
|
||||
);
|
||||
|
||||
$grade->updatedAt = $updatedAt;
|
||||
$grade->appreciation = $appreciation;
|
||||
$grade->appreciationUpdatedAt = $appreciationUpdatedAt;
|
||||
|
||||
return $grade;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Grade\AppreciationTemplate;
|
||||
use App\Scolarite\Domain\Model\Grade\AppreciationTemplateId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface AppreciationTemplateRepository
|
||||
{
|
||||
public function save(AppreciationTemplate $template): void;
|
||||
|
||||
public function findById(AppreciationTemplateId $id, TenantId $tenantId): ?AppreciationTemplate;
|
||||
|
||||
/** @return array<AppreciationTemplate> */
|
||||
public function findByTeacher(UserId $teacherId, TenantId $tenantId): array;
|
||||
|
||||
public function delete(AppreciationTemplateId $id, TenantId $tenantId): void;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyEvaluation;
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyEvaluationId;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
|
||||
interface CompetencyEvaluationRepository
|
||||
{
|
||||
public function save(CompetencyEvaluation $competencyEvaluation): void;
|
||||
|
||||
public function findById(CompetencyEvaluationId $id): ?CompetencyEvaluation;
|
||||
|
||||
/** @return array<CompetencyEvaluation> */
|
||||
public function findByEvaluation(EvaluationId $evaluationId): array;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyFramework;
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyFrameworkId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface CompetencyFrameworkRepository
|
||||
{
|
||||
public function save(CompetencyFramework $framework): void;
|
||||
|
||||
public function findById(CompetencyFrameworkId $id, TenantId $tenantId): ?CompetencyFramework;
|
||||
|
||||
public function findDefault(TenantId $tenantId): ?CompetencyFramework;
|
||||
|
||||
/** @return array<CompetencyFramework> */
|
||||
public function findByTenant(TenantId $tenantId): array;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Scolarite\Domain\Model\Competency\Competency;
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyFrameworkId;
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface CompetencyRepository
|
||||
{
|
||||
public function save(Competency $competency): void;
|
||||
|
||||
public function findById(CompetencyId $id, TenantId $tenantId): ?Competency;
|
||||
|
||||
/** @return array<Competency> */
|
||||
public function findByFramework(CompetencyFrameworkId $frameworkId): array;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Scolarite\Domain\Model\Competency\CustomCompetencyLevel;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface CustomCompetencyLevelRepository
|
||||
{
|
||||
public function save(CustomCompetencyLevel $level): void;
|
||||
|
||||
/** @return array<CustomCompetencyLevel> */
|
||||
public function findByTenant(TenantId $tenantId): array;
|
||||
|
||||
public function hasByTenant(TenantId $tenantId): bool;
|
||||
}
|
||||
@@ -5,11 +5,13 @@ declare(strict_types=1);
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
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\Exception\EvaluationNotFoundException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
interface EvaluationRepository
|
||||
{
|
||||
@@ -28,4 +30,16 @@ interface EvaluationRepository
|
||||
|
||||
/** @return array<Evaluation> */
|
||||
public function findByClass(ClassId $classId, TenantId $tenantId): array;
|
||||
|
||||
/** @return array<Evaluation> Toutes les évaluations publiées (notes visibles) */
|
||||
public function findAllWithPublishedGrades(TenantId $tenantId): array;
|
||||
|
||||
/** @return array<Evaluation> Évaluations dont les notes sont publiées */
|
||||
public function findWithPublishedGradesBySubjectAndClassInDateRange(
|
||||
SubjectId $subjectId,
|
||||
ClassId $classId,
|
||||
DateTimeImmutable $startDate,
|
||||
DateTimeImmutable $endDate,
|
||||
TenantId $tenantId,
|
||||
): array;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\ClassStatistics;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
|
||||
interface EvaluationStatisticsRepository
|
||||
{
|
||||
public function save(EvaluationId $evaluationId, ClassStatistics $statistics): void;
|
||||
|
||||
public function findByEvaluation(EvaluationId $evaluationId): ?ClassStatistics;
|
||||
|
||||
public function delete(EvaluationId $evaluationId): void;
|
||||
}
|
||||
@@ -14,6 +14,8 @@ interface GradeRepository
|
||||
{
|
||||
public function save(Grade $grade): void;
|
||||
|
||||
public function saveAppreciation(Grade $grade): void;
|
||||
|
||||
/** @throws GradeNotFoundException */
|
||||
public function get(GradeId $id, TenantId $tenantId): Grade;
|
||||
|
||||
@@ -22,5 +24,12 @@ interface GradeRepository
|
||||
/** @return array<Grade> */
|
||||
public function findByEvaluation(EvaluationId $evaluationId, TenantId $tenantId): array;
|
||||
|
||||
/**
|
||||
* @param array<EvaluationId> $evaluationIds
|
||||
*
|
||||
* @return array<string, list<Grade>> Clé = evaluationId (string)
|
||||
*/
|
||||
public function findByEvaluations(array $evaluationIds, TenantId $tenantId): array;
|
||||
|
||||
public function hasGradesForEvaluation(EvaluationId $evaluationId, TenantId $tenantId): bool;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface StudentAverageRepository
|
||||
{
|
||||
public function saveSubjectAverage(
|
||||
TenantId $tenantId,
|
||||
UserId $studentId,
|
||||
SubjectId $subjectId,
|
||||
string $periodId,
|
||||
float $average,
|
||||
int $gradeCount,
|
||||
): void;
|
||||
|
||||
public function saveGeneralAverage(
|
||||
TenantId $tenantId,
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
float $average,
|
||||
): void;
|
||||
|
||||
/** @return list<float> Moyennes matières d'un élève pour une période */
|
||||
public function findSubjectAveragesForStudent(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): array;
|
||||
|
||||
/** @return list<array{subjectId: string, subjectName: string|null, average: float, gradeCount: int}> */
|
||||
public function findDetailedSubjectAveragesForStudent(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): array;
|
||||
|
||||
public function findGeneralAverageForStudent(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): ?float;
|
||||
|
||||
public function deleteSubjectAverage(
|
||||
UserId $studentId,
|
||||
SubjectId $subjectId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): void;
|
||||
|
||||
public function deleteGeneralAverage(
|
||||
UserId $studentId,
|
||||
string $periodId,
|
||||
TenantId $tenantId,
|
||||
): void;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Repository;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyEvaluationId;
|
||||
use App\Scolarite\Domain\Model\Competency\CompetencyId;
|
||||
use App\Scolarite\Domain\Model\Competency\StudentCompetencyResult;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
interface StudentCompetencyResultRepository
|
||||
{
|
||||
public function save(StudentCompetencyResult $result): void;
|
||||
|
||||
public function delete(StudentCompetencyResult $result): void;
|
||||
|
||||
/** @return array<StudentCompetencyResult> */
|
||||
public function findByCompetencyEvaluation(
|
||||
CompetencyEvaluationId $competencyEvaluationId,
|
||||
TenantId $tenantId,
|
||||
): array;
|
||||
|
||||
public function findByCompetencyEvaluationAndStudent(
|
||||
CompetencyEvaluationId $competencyEvaluationId,
|
||||
UserId $studentId,
|
||||
TenantId $tenantId,
|
||||
): ?StudentCompetencyResult;
|
||||
|
||||
/** @return array<StudentCompetencyResult> */
|
||||
public function findByStudentAndCompetency(
|
||||
UserId $studentId,
|
||||
CompetencyId $competencyId,
|
||||
TenantId $tenantId,
|
||||
): array;
|
||||
}
|
||||
102
backend/src/Scolarite/Domain/Service/AverageCalculator.php
Normal file
102
backend/src/Scolarite/Domain/Service/AverageCalculator.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Service;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\ClassStatistics;
|
||||
|
||||
use function array_sum;
|
||||
use function count;
|
||||
use function intdiv;
|
||||
use function round;
|
||||
use function sort;
|
||||
|
||||
final class AverageCalculator
|
||||
{
|
||||
/**
|
||||
* Moyenne matière pondérée par les coefficients, normalisée sur /20.
|
||||
* Les notes absent/dispensé doivent être exclues en amont.
|
||||
*
|
||||
* Formule : Σ(note_sur_20 × coef) / Σ(coef)
|
||||
*
|
||||
* @param list<GradeEntry> $grades
|
||||
*/
|
||||
public function calculateSubjectAverage(array $grades): ?float
|
||||
{
|
||||
if ($grades === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sumWeighted = 0.0;
|
||||
$sumCoef = 0.0;
|
||||
|
||||
foreach ($grades as $entry) {
|
||||
$normalizedValue = $entry->gradeScale->convertTo20($entry->value);
|
||||
$sumWeighted += $normalizedValue * $entry->coefficient->value;
|
||||
$sumCoef += $entry->coefficient->value;
|
||||
}
|
||||
|
||||
return round($sumWeighted / $sumCoef, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moyenne générale : moyenne arithmétique des moyennes matières.
|
||||
* Les matières sans note sont exclues en amont.
|
||||
*
|
||||
* @param list<float> $subjectAverages
|
||||
*/
|
||||
public function calculateGeneralAverage(array $subjectAverages): ?float
|
||||
{
|
||||
if ($subjectAverages === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round(array_sum($subjectAverages) / count($subjectAverages), 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiques de classe pour une évaluation : min, max, moyenne, médiane.
|
||||
* Les absents et dispensés doivent être exclus en amont.
|
||||
*
|
||||
* @param list<float> $values
|
||||
*/
|
||||
public function calculateClassStatistics(array $values): ClassStatistics
|
||||
{
|
||||
if ($values === []) {
|
||||
return new ClassStatistics(
|
||||
average: null,
|
||||
min: null,
|
||||
max: null,
|
||||
median: null,
|
||||
gradedCount: 0,
|
||||
);
|
||||
}
|
||||
|
||||
sort($values);
|
||||
$count = count($values);
|
||||
|
||||
return new ClassStatistics(
|
||||
average: round(array_sum($values) / $count, 2),
|
||||
min: $values[0],
|
||||
max: $values[$count - 1],
|
||||
median: $this->calculateMedian($values),
|
||||
gradedCount: $count,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<float> $sortedValues Valeurs déjà triées par ordre croissant
|
||||
*/
|
||||
private function calculateMedian(array $sortedValues): float
|
||||
{
|
||||
$count = count($sortedValues);
|
||||
$middle = intdiv($count, 2);
|
||||
|
||||
if ($count % 2 === 0) {
|
||||
return round(($sortedValues[$middle - 1] + $sortedValues[$middle]) / 2, 2);
|
||||
}
|
||||
|
||||
return $sortedValues[$middle];
|
||||
}
|
||||
}
|
||||
18
backend/src/Scolarite/Domain/Service/GradeEntry.php
Normal file
18
backend/src/Scolarite/Domain/Service/GradeEntry.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Domain\Service;
|
||||
|
||||
use App\Scolarite\Domain\Model\Evaluation\Coefficient;
|
||||
use App\Scolarite\Domain\Model\Evaluation\GradeScale;
|
||||
|
||||
final readonly class GradeEntry
|
||||
{
|
||||
public function __construct(
|
||||
public float $value,
|
||||
public GradeScale $gradeScale,
|
||||
public Coefficient $coefficient,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user