feat: Gestion des périodes scolaires
L'administration d'un établissement nécessite de découper l'année scolaire en trimestres ou semestres avant de pouvoir saisir les notes et générer les bulletins. Ce module permet de configurer les périodes par année scolaire (current/previous/next résolus en UUID v5 déterministes), de modifier les dates individuelles avec validation anti-chevauchement, et de consulter la période en cours avec le décompte des jours restants. Les dates par défaut de février s'adaptent aux années bissextiles. Le repository utilise UPSERT transactionnel pour garantir l'intégrité lors du changement de mode (trimestres ↔ semestres). Les domain events de Subject sont étendus pour couvrir toutes les mutations (code, couleur, description) en plus du renommage.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Query\GetPeriods;
|
||||
|
||||
use App\Administration\Application\Query\GetPeriods\GetPeriodsHandler;
|
||||
use App\Administration\Application\Query\GetPeriods\GetPeriodsQuery;
|
||||
use App\Administration\Domain\Model\AcademicYear\DefaultPeriods;
|
||||
use App\Administration\Domain\Model\AcademicYear\PeriodType;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryPeriodConfigurationRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class GetPeriodsHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440002';
|
||||
|
||||
private InMemoryPeriodConfigurationRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemoryPeriodConfigurationRepository();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsNullWhenNoPeriodsConfigured(): void
|
||||
{
|
||||
$handler = $this->createHandler('2025-10-15');
|
||||
|
||||
$result = $handler(new GetPeriodsQuery(self::TENANT_ID, self::ACADEMIC_YEAR_ID));
|
||||
|
||||
self::assertNull($result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsPeriodsWithCurrentPeriodInfo(): void
|
||||
{
|
||||
$this->seedTrimesterConfig();
|
||||
$handler = $this->createHandler('2025-10-15');
|
||||
|
||||
$result = $handler(new GetPeriodsQuery(self::TENANT_ID, self::ACADEMIC_YEAR_ID));
|
||||
|
||||
self::assertNotNull($result);
|
||||
self::assertSame('trimester', $result->type);
|
||||
self::assertCount(3, $result->periods);
|
||||
|
||||
// T1 is current
|
||||
self::assertNotNull($result->currentPeriod);
|
||||
self::assertSame('T1', $result->currentPeriod->label);
|
||||
self::assertTrue($result->currentPeriod->isCurrent);
|
||||
self::assertSame(46, $result->currentPeriod->daysRemaining);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsNullCurrentPeriodWhenOutOfRange(): void
|
||||
{
|
||||
$this->seedTrimesterConfig();
|
||||
$handler = $this->createHandler('2025-08-15');
|
||||
|
||||
$result = $handler(new GetPeriodsQuery(self::TENANT_ID, self::ACADEMIC_YEAR_ID));
|
||||
|
||||
self::assertNotNull($result);
|
||||
self::assertNull($result->currentPeriod);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itMarksPastPeriods(): void
|
||||
{
|
||||
$this->seedTrimesterConfig();
|
||||
$handler = $this->createHandler('2026-04-15');
|
||||
|
||||
$result = $handler(new GetPeriodsQuery(self::TENANT_ID, self::ACADEMIC_YEAR_ID));
|
||||
|
||||
self::assertNotNull($result);
|
||||
self::assertTrue($result->periods[0]->isPast);
|
||||
self::assertTrue($result->periods[1]->isPast);
|
||||
self::assertFalse($result->periods[2]->isPast);
|
||||
}
|
||||
|
||||
private function seedTrimesterConfig(): void
|
||||
{
|
||||
$config = DefaultPeriods::forType(PeriodType::TRIMESTER, 2025);
|
||||
$this->repository->save(
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
||||
$config,
|
||||
);
|
||||
}
|
||||
|
||||
private function createHandler(string $dateString): GetPeriodsHandler
|
||||
{
|
||||
$clock = new class($dateString) implements Clock {
|
||||
public function __construct(private readonly string $dateString)
|
||||
{
|
||||
}
|
||||
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable($this->dateString);
|
||||
}
|
||||
};
|
||||
|
||||
return new GetPeriodsHandler($this->repository, $clock);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user