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,281 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Domain\Service;
use App\Scolarite\Domain\Service\TeacherStatisticsCalculator;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class TeacherStatisticsCalculatorTest extends TestCase
{
private TeacherStatisticsCalculator $calculator;
protected function setUp(): void
{
$this->calculator = new TeacherStatisticsCalculator();
}
// --- Distribution ---
#[Test]
public function distributionReturnsEmptyBinsWhenNoValues(): void
{
$bins = $this->calculator->calculateDistribution([]);
self::assertSame([0, 0, 0, 0, 0, 0, 0, 0], $bins);
}
#[Test]
public function distributionPlacesValuesInCorrectBins(): void
{
// Bins: [0-2.5[, [2.5-5[, [5-7.5[, [7.5-10[, [10-12.5[, [12.5-15[, [15-17.5[, [17.5-20]
$values = [1.0, 3.0, 6.0, 9.0, 11.0, 14.0, 16.0, 19.0];
$bins = $this->calculator->calculateDistribution($values);
self::assertSame([1, 1, 1, 1, 1, 1, 1, 1], $bins);
}
#[Test]
public function distributionHandlesMaxValue20InLastBin(): void
{
$bins = $this->calculator->calculateDistribution([20.0]);
self::assertSame([0, 0, 0, 0, 0, 0, 0, 1], $bins);
}
#[Test]
public function distributionHandlesMultipleValuesInSameBin(): void
{
$values = [10.0, 10.5, 11.0, 12.0];
$bins = $this->calculator->calculateDistribution($values);
// All in bin [10-12.5[
self::assertSame([0, 0, 0, 0, 4, 0, 0, 0], $bins);
}
#[Test]
public function distributionHandlesBoundaryValues(): void
{
$values = [0.0, 2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5];
$bins = $this->calculator->calculateDistribution($values);
// 0.0 → bin 0, 2.5 → bin 1, 5.0 → bin 2, 7.5 → bin 3
// 10.0 → bin 4, 12.5 → bin 5, 15.0 → bin 6, 17.5 → bin 7
self::assertSame([1, 1, 1, 1, 1, 1, 1, 1], $bins);
}
// --- Success Rate ---
#[Test]
public function successRateReturnsZeroWhenNoValues(): void
{
self::assertSame(0.0, $this->calculator->calculateSuccessRate([]));
}
#[Test]
public function successRateCountsValuesAboveThreshold(): void
{
// Threshold defaults to 10.0
$values = [8.0, 10.0, 12.0, 14.0, 6.0];
// 10.0, 12.0, 14.0 are >= 10 → 3/5 = 60%
self::assertSame(60.0, $this->calculator->calculateSuccessRate($values));
}
#[Test]
public function successRateWithCustomThreshold(): void
{
$values = [8.0, 10.0, 12.0, 14.0, 6.0];
// >= 12: 12.0, 14.0 → 2/5 = 40%
self::assertSame(40.0, $this->calculator->calculateSuccessRate($values, 12.0));
}
#[Test]
public function successRateWithAllAbove(): void
{
$values = [15.0, 18.0, 12.0];
self::assertSame(100.0, $this->calculator->calculateSuccessRate($values));
}
#[Test]
public function successRateWithNoneAbove(): void
{
$values = [4.0, 6.0, 8.0];
self::assertSame(0.0, $this->calculator->calculateSuccessRate($values));
}
// --- Trend Line ---
#[Test]
public function trendLineReturnsNullWhenLessThanTwoPoints(): void
{
self::assertNull($this->calculator->calculateTrendLine([]));
self::assertNull($this->calculator->calculateTrendLine([[1, 10.0]]));
}
#[Test]
public function trendLineCalculatesLinearRegression(): void
{
// Perfectly increasing line: y = 2x + 8
$points = [[1, 10.0], [2, 12.0], [3, 14.0]];
$result = $this->calculator->calculateTrendLine($points);
self::assertNotNull($result);
self::assertEqualsWithDelta(2.0, $result->slope, 0.01);
self::assertEqualsWithDelta(8.0, $result->intercept, 0.01);
}
#[Test]
public function trendLineWithFlatData(): void
{
$points = [[1, 12.0], [2, 12.0], [3, 12.0]];
$result = $this->calculator->calculateTrendLine($points);
self::assertNotNull($result);
self::assertEqualsWithDelta(0.0, $result->slope, 0.01);
}
#[Test]
public function trendLineWithDecreasingData(): void
{
$points = [[1, 16.0], [2, 14.0], [3, 12.0]];
$result = $this->calculator->calculateTrendLine($points);
self::assertNotNull($result);
self::assertLessThan(0, $result->slope);
}
// --- Detect Trend ---
#[Test]
public function detectTrendReturnsStableWhenLessThanTwoAverages(): void
{
self::assertSame('stable', $this->calculator->detectTrend([]));
self::assertSame('stable', $this->calculator->detectTrend([12.0]));
}
#[Test]
public function detectTrendReturnsImprovingWhenLastIsHigherByThreshold(): void
{
// Last - first > 1.0 (default threshold)
self::assertSame('improving', $this->calculator->detectTrend([10.0, 11.5, 13.0]));
}
#[Test]
public function detectTrendReturnsDecliningWhenLastIsLowerByThreshold(): void
{
self::assertSame('declining', $this->calculator->detectTrend([14.0, 12.0, 10.0]));
}
#[Test]
public function detectTrendReturnsStableWhenDifferenceIsBelowThreshold(): void
{
self::assertSame('stable', $this->calculator->detectTrend([12.0, 12.5, 12.8]));
}
// --- Percentile ---
#[Test]
public function percentileReturnsHundredWhenNoOtherValues(): void
{
self::assertSame(100.0, $this->calculator->calculatePercentile(12.0, []));
}
#[Test]
public function percentileCalculatesCorrectly(): void
{
// My value: 14.0, others: [10, 12, 16, 18]
// 2 out of 4 are below → 50th percentile
self::assertSame(50.0, $this->calculator->calculatePercentile(14.0, [10.0, 12.0, 16.0, 18.0]));
}
#[Test]
public function percentileAtBottom(): void
{
self::assertSame(0.0, $this->calculator->calculatePercentile(5.0, [10.0, 12.0, 14.0]));
}
#[Test]
public function percentileAtTop(): void
{
self::assertSame(100.0, $this->calculator->calculatePercentile(20.0, [10.0, 12.0, 14.0]));
}
#[Test]
public function percentileWithDuplicateValues(): void
{
// My value: 12.0, others: [12.0, 12.0, 12.0]
// 0 strictly below → 0th percentile
self::assertSame(0.0, $this->calculator->calculatePercentile(12.0, [12.0, 12.0, 12.0]));
}
// --- Detect Trend edge cases ---
#[Test]
public function detectTrendAtExactThresholdReturnsStable(): void
{
// Difference is exactly 1.0 → should be stable (not strictly above threshold)
self::assertSame('stable', $this->calculator->detectTrend([10.0, 11.0]));
}
#[Test]
public function detectTrendJustAboveThresholdReturnsImproving(): void
{
// Difference is 1.01 → should be improving
self::assertSame('improving', $this->calculator->detectTrend([10.0, 11.01]));
}
// --- Distribution edge cases ---
#[Test]
public function distributionWithSingleValue(): void
{
$bins = $this->calculator->calculateDistribution([10.0]);
self::assertSame([0, 0, 0, 0, 1, 0, 0, 0], $bins);
}
#[Test]
public function distributionWithAllSameValues(): void
{
$values = [12.0, 12.0, 12.0, 12.0, 12.0];
$bins = $this->calculator->calculateDistribution($values);
// All in bin [10-12.5[
self::assertSame([0, 0, 0, 0, 5, 0, 0, 0], $bins);
}
// --- Trend Line edge cases ---
#[Test]
public function trendLineWithTwoPointsExact(): void
{
$points = [[1, 10.0], [2, 14.0]];
$result = $this->calculator->calculateTrendLine($points);
self::assertNotNull($result);
self::assertEqualsWithDelta(4.0, $result->slope, 0.01);
self::assertEqualsWithDelta(6.0, $result->intercept, 0.01);
}
#[Test]
public function trendLineWithNoisyData(): void
{
// Noisy but overall increasing
$points = [[1, 8.0], [2, 12.0], [3, 10.0], [4, 14.0], [5, 13.0]];
$result = $this->calculator->calculateTrendLine($points);
self::assertNotNull($result);
self::assertGreaterThan(0, $result->slope);
}
}