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.
215 lines
6.7 KiB
PHP
215 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Infrastructure\Service;
|
|
|
|
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Infrastructure\Tenant\TenantConfig;
|
|
use App\Shared\Infrastructure\Tenant\TenantContext;
|
|
use App\Shared\Infrastructure\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class CurrentAcademicYearResolverTest extends TestCase
|
|
{
|
|
private const string TENANT_UUID = '550e8400-e29b-41d4-a716-446655440001';
|
|
|
|
#[Test]
|
|
public function itPassesThroughValidUuid(): void
|
|
{
|
|
$resolver = $this->createResolver('2026-02-05 10:00:00');
|
|
$uuid = '550e8400-e29b-41d4-a716-446655440099';
|
|
|
|
self::assertSame($uuid, $resolver->resolve($uuid));
|
|
}
|
|
|
|
#[Test]
|
|
public function itReturnsNullForInvalidIdentifier(): void
|
|
{
|
|
$resolver = $this->createResolver('2026-02-05 10:00:00');
|
|
|
|
self::assertNull($resolver->resolve('invalid'));
|
|
self::assertNull($resolver->resolve(''));
|
|
self::assertNull($resolver->resolve('past'));
|
|
}
|
|
|
|
#[Test]
|
|
public function itResolvesCurrentBeforeSeptember(): void
|
|
{
|
|
// February 2026 → school year 2025-2026
|
|
$resolver = $this->createResolver('2026-02-05 10:00:00');
|
|
$result = $resolver->resolve('current');
|
|
|
|
self::assertNotNull($result);
|
|
self::assertTrue(\Ramsey\Uuid\Uuid::isValid($result));
|
|
}
|
|
|
|
#[Test]
|
|
public function itResolvesCurrentAfterSeptember(): void
|
|
{
|
|
// October 2025 → school year 2025-2026
|
|
$resolver = $this->createResolver('2025-10-15 10:00:00');
|
|
$result = $resolver->resolve('current');
|
|
|
|
self::assertNotNull($result);
|
|
self::assertTrue(\Ramsey\Uuid\Uuid::isValid($result));
|
|
}
|
|
|
|
#[Test]
|
|
public function itResolvesSameUuidForSameSchoolYear(): void
|
|
{
|
|
// Both dates are in school year 2025-2026
|
|
$resolverOct = $this->createResolver('2025-10-15 10:00:00');
|
|
$resolverFeb = $this->createResolver('2026-02-05 10:00:00');
|
|
|
|
self::assertSame(
|
|
$resolverOct->resolve('current'),
|
|
$resolverFeb->resolve('current'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function itResolvesDifferentUuidForDifferentSchoolYears(): void
|
|
{
|
|
// October 2025 → 2025-2026, October 2026 → 2026-2027
|
|
$resolver2025 = $this->createResolver('2025-10-15 10:00:00');
|
|
$resolver2026 = $this->createResolver('2026-10-15 10:00:00');
|
|
|
|
self::assertNotSame(
|
|
$resolver2025->resolve('current'),
|
|
$resolver2026->resolve('current'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function itResolvesNextYear(): void
|
|
{
|
|
// February 2026, current = 2025-2026, next = 2026-2027
|
|
$resolver = $this->createResolver('2026-02-05 10:00:00');
|
|
|
|
$current = $resolver->resolve('current');
|
|
$next = $resolver->resolve('next');
|
|
|
|
self::assertNotNull($next);
|
|
self::assertTrue(\Ramsey\Uuid\Uuid::isValid($next));
|
|
self::assertNotSame($current, $next);
|
|
}
|
|
|
|
#[Test]
|
|
public function itResolvesPreviousYear(): void
|
|
{
|
|
// February 2026, current = 2025-2026, previous = 2024-2025
|
|
$resolver = $this->createResolver('2026-02-05 10:00:00');
|
|
|
|
$current = $resolver->resolve('current');
|
|
$previous = $resolver->resolve('previous');
|
|
|
|
self::assertNotNull($previous);
|
|
self::assertTrue(\Ramsey\Uuid\Uuid::isValid($previous));
|
|
self::assertNotSame($current, $previous);
|
|
}
|
|
|
|
#[Test]
|
|
public function nextOfCurrentYearMatchesCurrentOfNextYear(): void
|
|
{
|
|
// "next" from Feb 2026 should equal "current" from Oct 2026
|
|
$resolverFeb2026 = $this->createResolver('2026-02-05 10:00:00');
|
|
$resolverOct2026 = $this->createResolver('2026-10-15 10:00:00');
|
|
|
|
self::assertSame(
|
|
$resolverFeb2026->resolve('next'),
|
|
$resolverOct2026->resolve('current'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function previousOfCurrentYearMatchesCurrentOfPreviousYear(): void
|
|
{
|
|
// "previous" from Feb 2026 (2024-2025) should equal "current" from Oct 2024
|
|
$resolverFeb2026 = $this->createResolver('2026-02-05 10:00:00');
|
|
$resolverOct2024 = $this->createResolver('2024-10-15 10:00:00');
|
|
|
|
self::assertSame(
|
|
$resolverFeb2026->resolve('previous'),
|
|
$resolverOct2024->resolve('current'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function itResolvesDifferentUuidsForDifferentTenants(): void
|
|
{
|
|
$otherTenantUuid = '550e8400-e29b-41d4-a716-446655440099';
|
|
|
|
$resolver1 = $this->createResolver('2026-02-05 10:00:00', self::TENANT_UUID);
|
|
$resolver2 = $this->createResolver('2026-02-05 10:00:00', $otherTenantUuid);
|
|
|
|
self::assertNotSame(
|
|
$resolver1->resolve('current'),
|
|
$resolver2->resolve('current'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function itIsDeterministic(): void
|
|
{
|
|
$resolver = $this->createResolver('2026-02-05 10:00:00');
|
|
|
|
self::assertSame(
|
|
$resolver->resolve('current'),
|
|
$resolver->resolve('current'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function septemberBelongsToNewSchoolYear(): void
|
|
{
|
|
// September 1st 2026 should be in school year 2026-2027
|
|
$resolverSept = $this->createResolver('2026-09-01 08:00:00');
|
|
$resolverOct = $this->createResolver('2026-10-15 10:00:00');
|
|
|
|
self::assertSame(
|
|
$resolverSept->resolve('current'),
|
|
$resolverOct->resolve('current'),
|
|
);
|
|
}
|
|
|
|
#[Test]
|
|
public function augustBelongsToPreviousSchoolYear(): void
|
|
{
|
|
// August 31st 2026 should still be in school year 2025-2026
|
|
$resolverAug = $this->createResolver('2026-08-31 23:59:59');
|
|
$resolverFeb = $this->createResolver('2026-02-05 10:00:00');
|
|
|
|
self::assertSame(
|
|
$resolverAug->resolve('current'),
|
|
$resolverFeb->resolve('current'),
|
|
);
|
|
}
|
|
|
|
private function createResolver(string $dateTime, string $tenantUuid = self::TENANT_UUID): CurrentAcademicYearResolver
|
|
{
|
|
$tenantContext = new TenantContext();
|
|
$tenantContext->setCurrentTenant(new TenantConfig(
|
|
tenantId: TenantId::fromString($tenantUuid),
|
|
subdomain: 'test',
|
|
databaseUrl: 'sqlite:///:memory:',
|
|
));
|
|
|
|
$clock = new class($dateTime) implements Clock {
|
|
public function __construct(private readonly string $dateTime)
|
|
{
|
|
}
|
|
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable($this->dateTime);
|
|
}
|
|
};
|
|
|
|
return new CurrentAcademicYearResolver($tenantContext, $clock);
|
|
}
|
|
}
|