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 e72867932d
107 changed files with 9709 additions and 383 deletions

View File

@@ -0,0 +1,109 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Application\Query\GetClassStatisticsDetail;
use App\Scolarite\Application\Port\PeriodFinder;
use App\Scolarite\Application\Port\PeriodInfo;
use App\Scolarite\Application\Query\GetClassStatisticsDetail\GetClassStatisticsDetailHandler;
use App\Scolarite\Application\Query\GetClassStatisticsDetail\GetClassStatisticsDetailQuery;
use App\Scolarite\Domain\Service\AverageCalculator;
use App\Scolarite\Domain\Service\TeacherStatisticsCalculator;
use App\Scolarite\Infrastructure\ReadModel\InMemoryTeacherStatisticsReader;
use App\Shared\Domain\Tenant\TenantId;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class GetClassStatisticsDetailHandlerTest extends TestCase
{
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
private InMemoryTeacherStatisticsReader $reader;
protected function setUp(): void
{
$this->reader = new InMemoryTeacherStatisticsReader();
}
#[Test]
public function itReturnsEmptyWhenNoPeriodFound(): void
{
$handler = $this->createHandler(periodInfo: null);
$result = $handler($this->query());
self::assertNull($result->average);
self::assertSame(0.0, $result->successRate);
self::assertSame([], $result->students);
}
#[Test]
public function itComputesClassStatisticsFromGrades(): void
{
$this->reader->feedClassGrades([8.0, 10.0, 12.0, 14.0, 16.0]);
$this->reader->feedMonthlyAverages([
['month' => '2026-01', 'average' => 11.0],
['month' => '2026-02', 'average' => 12.5],
]);
$this->reader->feedStudentAverages([
['studentId' => 's1', 'studentName' => 'Alice Dupont', 'average' => 14.0],
['studentId' => 's2', 'studentName' => 'Bob Martin', 'average' => 7.0],
]);
$handler = $this->createHandler(periodInfo: $this->currentPeriod());
$result = $handler($this->query());
self::assertSame(12.0, $result->average);
self::assertSame(80.0, $result->successRate); // 4/5 >= 10
self::assertSame([0, 0, 0, 1, 2, 1, 1, 0], $result->distribution);
self::assertCount(2, $result->evolution);
self::assertCount(2, $result->students);
self::assertFalse($result->students[0]->inDifficulty); // Alice 14.0 >= 8.0
self::assertTrue($result->students[1]->inDifficulty); // Bob 7.0 < 8.0
}
private function query(): GetClassStatisticsDetailQuery
{
return new GetClassStatisticsDetailQuery(
teacherId: self::TEACHER_ID,
classId: self::CLASS_ID,
subjectId: self::SUBJECT_ID,
tenantId: self::TENANT_ID,
);
}
private function createHandler(?PeriodInfo $periodInfo): GetClassStatisticsDetailHandler
{
$periodFinder = new class($periodInfo) implements PeriodFinder {
public function __construct(private readonly ?PeriodInfo $info)
{
}
public function findForDate(DateTimeImmutable $date, TenantId $tenantId): ?PeriodInfo
{
return $this->info;
}
};
return new GetClassStatisticsDetailHandler(
$this->reader,
$periodFinder,
new TeacherStatisticsCalculator(),
new AverageCalculator(),
);
}
private function currentPeriod(): PeriodInfo
{
return new PeriodInfo(
periodId: 'period-1',
startDate: new DateTimeImmutable('2026-01-05'),
endDate: new DateTimeImmutable('2026-03-31'),
);
}
}