feat: Provisionner automatiquement un nouvel établissement
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Lorsqu'un super-admin crée un établissement via l'interface, le système
doit automatiquement créer la base tenant, exécuter les migrations,
créer le premier utilisateur admin et envoyer l'invitation — le tout
de manière asynchrone pour ne pas bloquer la réponse HTTP.

Ce mécanisme rend chaque établissement opérationnel dès sa création
sans intervention manuelle sur l'infrastructure.
This commit is contained in:
2026-04-08 13:55:41 +02:00
parent bec211ebf0
commit 3575d095a1
106 changed files with 9586 additions and 380 deletions

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
final readonly class ClassStatisticsDetailDto
{
/**
* @param list<int> $distribution
* @param list<array{month: string, average: float}> $evolution
* @param list<StudentAverageDto> $students
*/
public function __construct(
public ?float $average,
public float $successRate,
public array $distribution,
public array $evolution,
public array $students,
) {
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
use App\Scolarite\Application\Port\PeriodFinder;
use App\Scolarite\Application\Port\TeacherStatisticsReader;
use App\Scolarite\Domain\Service\AverageCalculator;
use App\Scolarite\Domain\Service\TeacherStatisticsCalculator;
use App\Shared\Domain\Tenant\TenantId;
use function array_map;
use function count;
use DateTimeImmutable;
use function sprintf;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(bus: 'query.bus')]
final readonly class GetClassStatisticsDetailHandler
{
public function __construct(
private TeacherStatisticsReader $reader,
private PeriodFinder $periodFinder,
private TeacherStatisticsCalculator $statisticsCalculator,
private AverageCalculator $averageCalculator,
) {
}
public function __invoke(GetClassStatisticsDetailQuery $query): ClassStatisticsDetailDto
{
$tenantId = TenantId::fromString($query->tenantId);
$period = $this->periodFinder->findForDate(new DateTimeImmutable(), $tenantId);
if ($period === null) {
return new ClassStatisticsDetailDto(
average: null,
successRate: 0.0,
distribution: [0, 0, 0, 0, 0, 0, 0, 0],
evolution: [],
students: [],
);
}
$normalizedGrades = $this->reader->classGradesNormalized(
$query->teacherId,
$query->classId,
$query->subjectId,
$query->tenantId,
$period->startDate->format('Y-m-d'),
$period->endDate->format('Y-m-d'),
);
$classStats = $this->averageCalculator->calculateClassStatistics($normalizedGrades);
$distribution = $this->statisticsCalculator->calculateDistribution($normalizedGrades);
$successRate = $this->statisticsCalculator->calculateSuccessRate($normalizedGrades);
$now = new DateTimeImmutable();
$month = (int) $now->format('n');
$yearStart = $month >= 9 ? (int) $now->format('Y') : (int) $now->format('Y') - 1;
$academicYearStart = sprintf('%d-09-01', $yearStart);
$academicYearEnd = sprintf('%d-08-31', $yearStart + 1);
$evolution = $this->reader->classMonthlyAverages(
$query->teacherId,
$query->classId,
$query->subjectId,
$query->tenantId,
$academicYearStart,
$academicYearEnd,
);
$studentAverages = $this->reader->studentAveragesForClass(
$query->teacherId,
$query->classId,
$query->subjectId,
$period->periodId,
$query->tenantId,
);
$studentTrends = $this->reader->studentMonthlyAveragesForClass(
$query->classId,
$query->subjectId,
$query->tenantId,
$academicYearStart,
$academicYearEnd,
);
$students = array_map(
fn (array $s) => new StudentAverageDto(
studentId: $s['studentId'],
studentName: $s['studentName'],
average: $s['average'],
inDifficulty: $s['average'] !== null && $s['average'] < $query->threshold,
trend: isset($studentTrends[$s['studentId']]) && count($studentTrends[$s['studentId']]) >= 2
? $this->statisticsCalculator->detectTrend($studentTrends[$s['studentId']])
: 'stable',
),
$studentAverages,
);
return new ClassStatisticsDetailDto(
average: $classStats->average,
successRate: $successRate,
distribution: $distribution,
evolution: $evolution,
students: $students,
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
final readonly class GetClassStatisticsDetailQuery
{
public function __construct(
public string $teacherId,
public string $classId,
public string $subjectId,
public string $tenantId,
public float $threshold = 8.0,
) {
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
final readonly class StudentAverageDto
{
public function __construct(
public string $studentId,
public string $studentName,
public ?float $average,
public bool $inDifficulty,
public string $trend,
) {
}
}