feat: Provisionner automatiquement un nouvel établissement
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:
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user