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.
185 lines
6.2 KiB
PHP
185 lines
6.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Application\Command\UpdatePeriod;
|
|
|
|
use App\Administration\Application\Command\UpdatePeriod\UpdatePeriodCommand;
|
|
use App\Administration\Application\Command\UpdatePeriod\UpdatePeriodHandler;
|
|
use App\Administration\Application\Port\GradeExistenceChecker;
|
|
use App\Administration\Domain\Exception\PeriodeAvecNotesException;
|
|
use App\Administration\Domain\Exception\PeriodeNonTrouveeException;
|
|
use App\Administration\Domain\Exception\PeriodesNonConfigureesException;
|
|
use App\Administration\Domain\Exception\PeriodsOverlapException;
|
|
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\Administration\Infrastructure\Service\NoOpGradeExistenceChecker;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use Override;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Component\Messenger\Envelope;
|
|
use Symfony\Component\Messenger\MessageBusInterface;
|
|
|
|
final class UpdatePeriodHandlerTest 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;
|
|
private UpdatePeriodHandler $handler;
|
|
private Clock $clock;
|
|
private MessageBusInterface $eventBus;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->repository = new InMemoryPeriodConfigurationRepository();
|
|
$this->clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2025-10-15 10:00:00');
|
|
}
|
|
};
|
|
$this->eventBus = new class implements MessageBusInterface {
|
|
public function dispatch(object $message, array $stamps = []): Envelope
|
|
{
|
|
return new Envelope($message);
|
|
}
|
|
};
|
|
$this->handler = new UpdatePeriodHandler($this->repository, new NoOpGradeExistenceChecker(), $this->clock, $this->eventBus);
|
|
}
|
|
|
|
#[Test]
|
|
public function itUpdatesPeriodDates(): void
|
|
{
|
|
$this->seedTrimesterConfig();
|
|
|
|
$command = new UpdatePeriodCommand(
|
|
tenantId: self::TENANT_ID,
|
|
academicYearId: self::ACADEMIC_YEAR_ID,
|
|
sequence: 1,
|
|
startDate: '2025-09-01',
|
|
endDate: '2025-12-15',
|
|
);
|
|
|
|
$this->expectException(PeriodsOverlapException::class);
|
|
($this->handler)($command);
|
|
}
|
|
|
|
#[Test]
|
|
public function itUpdatesValidPeriodDates(): void
|
|
{
|
|
$this->seedTrimesterConfig();
|
|
|
|
$command = new UpdatePeriodCommand(
|
|
tenantId: self::TENANT_ID,
|
|
academicYearId: self::ACADEMIC_YEAR_ID,
|
|
sequence: 1,
|
|
startDate: '2025-09-02',
|
|
endDate: '2025-11-30',
|
|
);
|
|
|
|
$config = ($this->handler)($command);
|
|
|
|
self::assertSame('2025-09-02', $config->periods[0]->startDate->format('Y-m-d'));
|
|
self::assertSame('2025-11-30', $config->periods[0]->endDate->format('Y-m-d'));
|
|
}
|
|
|
|
#[Test]
|
|
public function itRejectsWhenNoConfigurationExists(): void
|
|
{
|
|
$this->expectException(PeriodesNonConfigureesException::class);
|
|
|
|
($this->handler)(new UpdatePeriodCommand(
|
|
tenantId: self::TENANT_ID,
|
|
academicYearId: self::ACADEMIC_YEAR_ID,
|
|
sequence: 1,
|
|
startDate: '2025-09-01',
|
|
endDate: '2025-11-30',
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itRejectsUnknownSequence(): void
|
|
{
|
|
$this->seedTrimesterConfig();
|
|
|
|
$this->expectException(PeriodeNonTrouveeException::class);
|
|
|
|
($this->handler)(new UpdatePeriodCommand(
|
|
tenantId: self::TENANT_ID,
|
|
academicYearId: self::ACADEMIC_YEAR_ID,
|
|
sequence: 99,
|
|
startDate: '2025-09-01',
|
|
endDate: '2025-11-30',
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itRejectsUpdateWhenPeriodHasGradesWithoutConfirmation(): void
|
|
{
|
|
$this->seedTrimesterConfig();
|
|
|
|
$gradeChecker = new class implements GradeExistenceChecker {
|
|
#[Override]
|
|
public function hasGradesInPeriod(TenantId $tenantId, AcademicYearId $academicYearId, int $periodSequence): bool
|
|
{
|
|
return true;
|
|
}
|
|
};
|
|
|
|
$handler = new UpdatePeriodHandler($this->repository, $gradeChecker, $this->clock, $this->eventBus);
|
|
|
|
$this->expectException(PeriodeAvecNotesException::class);
|
|
|
|
$handler(new UpdatePeriodCommand(
|
|
tenantId: self::TENANT_ID,
|
|
academicYearId: self::ACADEMIC_YEAR_ID,
|
|
sequence: 1,
|
|
startDate: '2025-09-02',
|
|
endDate: '2025-11-30',
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function itAllowsUpdateWhenPeriodHasGradesWithConfirmation(): void
|
|
{
|
|
$this->seedTrimesterConfig();
|
|
|
|
$gradeChecker = new class implements GradeExistenceChecker {
|
|
#[Override]
|
|
public function hasGradesInPeriod(TenantId $tenantId, AcademicYearId $academicYearId, int $periodSequence): bool
|
|
{
|
|
return true;
|
|
}
|
|
};
|
|
|
|
$handler = new UpdatePeriodHandler($this->repository, $gradeChecker, $this->clock, $this->eventBus);
|
|
|
|
$config = $handler(new UpdatePeriodCommand(
|
|
tenantId: self::TENANT_ID,
|
|
academicYearId: self::ACADEMIC_YEAR_ID,
|
|
sequence: 1,
|
|
startDate: '2025-09-02',
|
|
endDate: '2025-11-30',
|
|
confirmImpact: true,
|
|
));
|
|
|
|
self::assertSame('2025-09-02', $config->periods[0]->startDate->format('Y-m-d'));
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|