Les établissements scolaires utilisent des systèmes d'évaluation variés (notes /20, /10, lettres, compétences, sans notes). Jusqu'ici l'application imposait implicitement le mode notes /20, ce qui ne correspondait pas à la réalité pédagogique de nombreuses écoles. Cette configuration permet à chaque établissement de choisir son mode de notation par année scolaire, avec verrouillage automatique dès que des notes ont été saisies pour éviter les incohérences. Le Score Sérénité adapte ses pondérations selon le mode choisi (les compétences sont converties via un mapping, le mode sans notes exclut la composante notes).
191 lines
6.3 KiB
PHP
191 lines
6.3 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\Domain\Model\SchoolClass\SchoolId;
|
|
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 itRejectsOverlappingPeriodDates(): 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 = $this->createGradeCheckerWithGrades();
|
|
|
|
$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 = $this->createGradeCheckerWithGrades();
|
|
|
|
$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 createGradeCheckerWithGrades(): GradeExistenceChecker
|
|
{
|
|
return new class implements GradeExistenceChecker {
|
|
#[Override]
|
|
public function hasGradesInPeriod(TenantId $tenantId, AcademicYearId $academicYearId, int $periodSequence): bool
|
|
{
|
|
return true;
|
|
}
|
|
|
|
#[Override]
|
|
public function hasGradesForYear(TenantId $tenantId, SchoolId $schoolId, AcademicYearId $academicYearId): bool
|
|
{
|
|
return true;
|
|
}
|
|
};
|
|
}
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|