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,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user