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,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Domain\Repository\StudentAverageRepository;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\StudentAveragesResource;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
|
||||
use function in_array;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Override;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<StudentAveragesResource>
|
||||
*/
|
||||
final readonly class StudentAveragesProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private StudentAverageRepository $studentAverageRepository,
|
||||
private TenantContext $tenantContext,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): StudentAveragesResource
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
/** @var string $studentId */
|
||||
$studentId = $uriVariables['studentId'];
|
||||
|
||||
try {
|
||||
$userId = UserId::fromString($studentId);
|
||||
} catch (InvalidArgumentException) {
|
||||
throw new BadRequestHttpException('Identifiant d\'élève invalide.');
|
||||
}
|
||||
|
||||
// L'élève peut voir ses propres moyennes, les enseignants et admins aussi
|
||||
$isOwner = $user->userId() === $studentId;
|
||||
$isStaff = $this->hasAnyRole($user->getRoles(), [
|
||||
Role::ADMIN->value,
|
||||
Role::PROF->value,
|
||||
Role::VIE_SCOLAIRE->value,
|
||||
]);
|
||||
|
||||
if (!$isOwner && !$isStaff) {
|
||||
throw new AccessDeniedHttpException('Accès non autorisé aux moyennes de cet élève.');
|
||||
}
|
||||
|
||||
$tenantId = $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
/** @var array<string, mixed> $filters */
|
||||
$filters = $context['filters'] ?? [];
|
||||
/** @var string|null $periodId */
|
||||
$periodId = $filters['periodId'] ?? null;
|
||||
|
||||
$resource = new StudentAveragesResource();
|
||||
$resource->studentId = $studentId;
|
||||
$resource->periodId = $periodId;
|
||||
|
||||
if ($periodId === null) {
|
||||
return $resource;
|
||||
}
|
||||
|
||||
$resource->subjectAverages = $this->studentAverageRepository->findDetailedSubjectAveragesForStudent(
|
||||
$userId,
|
||||
$periodId,
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
$resource->generalAverage = $this->studentAverageRepository->findGeneralAverageForStudent(
|
||||
$userId,
|
||||
$periodId,
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $userRoles
|
||||
* @param list<string> $allowedRoles
|
||||
*/
|
||||
private function hasAnyRole(array $userRoles, array $allowedRoles): bool
|
||||
{
|
||||
foreach ($userRoles as $role) {
|
||||
if (in_array($role, $allowedRoles, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user