feat: Configurer les jours fériés et vacances du calendrier scolaire
Les administrateurs d'établissement avaient besoin de gérer le calendrier scolaire (FR80) pour que l'EDT et les devoirs respectent automatiquement les jours non travaillés. Sans cette configuration centralisée, chaque module devait gérer indépendamment les contraintes de dates. Le calendrier s'appuie sur l'API data.education.gouv.fr pour importer les vacances officielles par zone (A/B/C) et calcule les 11 jours fériés français (dont les fêtes mobiles liées à Pâques). Les enseignants sont notifiés par email lors de l'ajout d'une journée pédagogique. Un query IsSchoolDay et une validation des dates d'échéance de devoirs permettent aux autres modules de s'intégrer sans couplage direct.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\AddPedagogicalDay;
|
||||
|
||||
use App\Administration\Application\Command\AddPedagogicalDay\AddPedagogicalDayCommand;
|
||||
use App\Administration\Application\Command\AddPedagogicalDay\AddPedagogicalDayHandler;
|
||||
use App\Administration\Domain\Event\JourneePedagogiqueAjoutee;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolCalendarRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AddPedagogicalDayHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemorySchoolCalendarRepository $repository;
|
||||
private AddPedagogicalDayHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemorySchoolCalendarRepository();
|
||||
$clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-17 10:00:00');
|
||||
}
|
||||
};
|
||||
|
||||
$this->handler = new AddPedagogicalDayHandler(
|
||||
calendarRepository: $this->repository,
|
||||
clock: $clock,
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAddsPedagogicalDayToExistingCalendar(): void
|
||||
{
|
||||
$this->seedCalendar();
|
||||
|
||||
$command = new AddPedagogicalDayCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-03-14',
|
||||
label: 'Formation enseignants',
|
||||
);
|
||||
|
||||
$calendar = ($this->handler)($command);
|
||||
|
||||
$entries = $calendar->entries();
|
||||
$pedagogicalDays = array_filter(
|
||||
$entries,
|
||||
static fn ($e) => $e->type === CalendarEntryType::PEDAGOGICAL_DAY,
|
||||
);
|
||||
|
||||
self::assertCount(1, $pedagogicalDays);
|
||||
$day = array_values($pedagogicalDays)[0];
|
||||
self::assertSame('Formation enseignants', $day->label);
|
||||
self::assertSame('2025-03-14', $day->startDate->format('Y-m-d'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesNewCalendarIfNoneExists(): void
|
||||
{
|
||||
$command = new AddPedagogicalDayCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-03-14',
|
||||
label: 'Formation enseignants',
|
||||
);
|
||||
|
||||
$calendar = ($this->handler)($command);
|
||||
|
||||
self::assertCount(1, $calendar->entries());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRecordsJourneePedagogiqueAjouteeEvent(): void
|
||||
{
|
||||
$command = new AddPedagogicalDayCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-03-14',
|
||||
label: 'Formation enseignants',
|
||||
);
|
||||
|
||||
$calendar = ($this->handler)($command);
|
||||
|
||||
$events = $calendar->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(JourneePedagogiqueAjoutee::class, $events[0]);
|
||||
self::assertSame('Formation enseignants', $events[0]->label);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSavesCalendarWithPedagogicalDay(): void
|
||||
{
|
||||
$command = new AddPedagogicalDayCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-03-14',
|
||||
label: 'Formation',
|
||||
description: 'Journée de formation continue',
|
||||
);
|
||||
|
||||
($this->handler)($command);
|
||||
|
||||
$saved = $this->repository->getByTenantAndYear(
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
||||
);
|
||||
|
||||
self::assertCount(1, $saved->entries());
|
||||
self::assertSame('Journée de formation continue', $saved->entries()[0]->description);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRejectsMalformedDate(): void
|
||||
{
|
||||
$command = new AddPedagogicalDayCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: 'not-a-date',
|
||||
label: 'Formation',
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('La date doit être au format YYYY-MM-DD.');
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRejectsImpossibleCalendarDate(): void
|
||||
{
|
||||
$command = new AddPedagogicalDayCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-06-31',
|
||||
label: 'Formation',
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('La date n\'existe pas dans le calendrier.');
|
||||
|
||||
($this->handler)($command);
|
||||
}
|
||||
|
||||
private function seedCalendar(): void
|
||||
{
|
||||
$calendar = SchoolCalendar::initialiser(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
||||
);
|
||||
|
||||
$this->repository->save($calendar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\ConfigureCalendar;
|
||||
|
||||
use App\Administration\Application\Command\ConfigureCalendar\ConfigureCalendarCommand;
|
||||
use App\Administration\Application\Command\ConfigureCalendar\ConfigureCalendarHandler;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolZone;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolCalendarRepository;
|
||||
use App\Administration\Infrastructure\Service\FrenchPublicHolidaysCalculator;
|
||||
use App\Administration\Infrastructure\Service\JsonOfficialCalendarProvider;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function json_encode;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\HttpClient\Response\MockResponse;
|
||||
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
final class ConfigureCalendarHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemorySchoolCalendarRepository $repository;
|
||||
private ConfigureCalendarHandler $handler;
|
||||
private string $tempDir;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tempDir = sys_get_temp_dir() . '/classeo-handler-test-' . uniqid();
|
||||
$this->repository = new InMemorySchoolCalendarRepository();
|
||||
$clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-17 10:00:00');
|
||||
}
|
||||
};
|
||||
|
||||
$this->handler = new ConfigureCalendarHandler(
|
||||
calendarRepository: $this->repository,
|
||||
calendarProvider: new JsonOfficialCalendarProvider(
|
||||
dataDirectory: $this->tempDir,
|
||||
httpClient: new MockHttpClient($this->mockApiResponse()),
|
||||
holidaysCalculator: new FrenchPublicHolidaysCalculator(),
|
||||
logger: new NullLogger(),
|
||||
),
|
||||
clock: $clock,
|
||||
);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$files = glob($this->tempDir . '/*');
|
||||
if ($files !== false) {
|
||||
foreach ($files as $file) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
if (is_dir($this->tempDir)) {
|
||||
rmdir($this->tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itConfiguresCalendarWithZoneA(): void
|
||||
{
|
||||
$command = new ConfigureCalendarCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
zone: 'A',
|
||||
academicYear: '2024-2025',
|
||||
);
|
||||
|
||||
$calendar = ($this->handler)($command);
|
||||
|
||||
self::assertSame(SchoolZone::A, $calendar->zone);
|
||||
self::assertNotEmpty($calendar->entries());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itImportsHolidaysAndVacations(): void
|
||||
{
|
||||
$command = new ConfigureCalendarCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
zone: 'B',
|
||||
academicYear: '2024-2025',
|
||||
);
|
||||
|
||||
$calendar = ($this->handler)($command);
|
||||
|
||||
$holidays = array_filter(
|
||||
$calendar->entries(),
|
||||
static fn ($e) => $e->type === CalendarEntryType::HOLIDAY,
|
||||
);
|
||||
$vacations = array_filter(
|
||||
$calendar->entries(),
|
||||
static fn ($e) => $e->type === CalendarEntryType::VACATION,
|
||||
);
|
||||
|
||||
self::assertNotEmpty($holidays);
|
||||
self::assertNotEmpty($vacations);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSavesCalendarToRepository(): void
|
||||
{
|
||||
$command = new ConfigureCalendarCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
zone: 'C',
|
||||
academicYear: '2024-2025',
|
||||
);
|
||||
|
||||
($this->handler)($command);
|
||||
|
||||
$saved = $this->repository->findByTenantAndYear(
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
||||
);
|
||||
|
||||
self::assertNotNull($saved);
|
||||
self::assertSame(SchoolZone::C, $saved->zone);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReconfiguresExistingCalendar(): void
|
||||
{
|
||||
$command = new ConfigureCalendarCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
zone: 'A',
|
||||
academicYear: '2024-2025',
|
||||
);
|
||||
|
||||
($this->handler)($command);
|
||||
|
||||
$commandB = new ConfigureCalendarCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
zone: 'B',
|
||||
academicYear: '2024-2025',
|
||||
);
|
||||
|
||||
$calendar = ($this->handler)($commandB);
|
||||
|
||||
self::assertSame(SchoolZone::B, $calendar->zone);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRecordsCalendrierConfigureEvent(): void
|
||||
{
|
||||
$command = new ConfigureCalendarCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
zone: 'A',
|
||||
academicYear: '2024-2025',
|
||||
);
|
||||
|
||||
$calendar = ($this->handler)($command);
|
||||
|
||||
$events = $calendar->pullDomainEvents();
|
||||
self::assertNotEmpty($events);
|
||||
}
|
||||
|
||||
private function mockApiResponse(): MockResponse
|
||||
{
|
||||
$records = [
|
||||
['description' => 'Vacances de la Toussaint', 'start_date' => '2024-10-18T23:00:00+00:00', 'end_date' => '2024-11-03T23:00:00+00:00', 'zones' => 'Zone A'],
|
||||
['description' => 'Vacances de Noël', 'start_date' => '2024-12-20T23:00:00+00:00', 'end_date' => '2025-01-05T23:00:00+00:00', 'zones' => 'Zone A'],
|
||||
['description' => 'Vacances d\'hiver', 'start_date' => '2025-02-21T23:00:00+00:00', 'end_date' => '2025-03-09T23:00:00+00:00', 'zones' => 'Zone A'],
|
||||
['description' => 'Vacances de printemps', 'start_date' => '2025-04-18T22:00:00+00:00', 'end_date' => '2025-05-04T22:00:00+00:00', 'zones' => 'Zone A'],
|
||||
['description' => 'Vacances d\'été', 'start_date' => '2025-07-04T22:00:00+00:00', 'end_date' => '2025-08-31T22:00:00+00:00', 'zones' => 'Zone A'],
|
||||
['description' => 'Vacances de la Toussaint', 'start_date' => '2024-10-18T23:00:00+00:00', 'end_date' => '2024-11-03T23:00:00+00:00', 'zones' => 'Zone B'],
|
||||
['description' => 'Vacances de Noël', 'start_date' => '2024-12-20T23:00:00+00:00', 'end_date' => '2025-01-05T23:00:00+00:00', 'zones' => 'Zone B'],
|
||||
['description' => 'Vacances d\'hiver', 'start_date' => '2025-02-07T23:00:00+00:00', 'end_date' => '2025-02-23T23:00:00+00:00', 'zones' => 'Zone B'],
|
||||
['description' => 'Vacances de printemps', 'start_date' => '2025-04-04T22:00:00+00:00', 'end_date' => '2025-04-20T22:00:00+00:00', 'zones' => 'Zone B'],
|
||||
['description' => 'Vacances d\'été', 'start_date' => '2025-07-04T22:00:00+00:00', 'end_date' => '2025-08-31T22:00:00+00:00', 'zones' => 'Zone B'],
|
||||
['description' => 'Vacances de la Toussaint', 'start_date' => '2024-10-18T23:00:00+00:00', 'end_date' => '2024-11-03T23:00:00+00:00', 'zones' => 'Zone C'],
|
||||
['description' => 'Vacances de Noël', 'start_date' => '2024-12-20T23:00:00+00:00', 'end_date' => '2025-01-05T23:00:00+00:00', 'zones' => 'Zone C'],
|
||||
['description' => 'Vacances d\'hiver', 'start_date' => '2025-02-14T23:00:00+00:00', 'end_date' => '2025-03-02T23:00:00+00:00', 'zones' => 'Zone C'],
|
||||
['description' => 'Vacances de printemps', 'start_date' => '2025-04-11T22:00:00+00:00', 'end_date' => '2025-04-27T22:00:00+00:00', 'zones' => 'Zone C'],
|
||||
['description' => 'Vacances d\'été', 'start_date' => '2025-07-04T22:00:00+00:00', 'end_date' => '2025-08-31T22:00:00+00:00', 'zones' => 'Zone C'],
|
||||
];
|
||||
|
||||
return new MockResponse(json_encode(['results' => $records], JSON_THROW_ON_ERROR), ['http_code' => 200]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Query\IsSchoolDay;
|
||||
|
||||
use App\Administration\Application\Query\IsSchoolDay\IsSchoolDayHandler;
|
||||
use App\Administration\Application\Query\IsSchoolDay\IsSchoolDayQuery;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntry;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryId;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolCalendarRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class IsSchoolDayHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemorySchoolCalendarRepository $repository;
|
||||
private IsSchoolDayHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemorySchoolCalendarRepository();
|
||||
$this->handler = new IsSchoolDayHandler(
|
||||
calendarRepository: $this->repository,
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function weekdayWithNoCalendarIsSchoolDay(): void
|
||||
{
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-03-10', // Lundi
|
||||
);
|
||||
|
||||
self::assertTrue(($this->handler)($query));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function saturdayIsNotSchoolDay(): void
|
||||
{
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-03-15', // Samedi
|
||||
);
|
||||
|
||||
self::assertFalse(($this->handler)($query));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function sundayIsNotSchoolDay(): void
|
||||
{
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-03-16', // Dimanche
|
||||
);
|
||||
|
||||
self::assertFalse(($this->handler)($query));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function holidayIsNotSchoolDay(): void
|
||||
{
|
||||
$this->seedCalendarWithHoliday('2025-05-01', 'Fête du travail');
|
||||
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-05-01', // Jeudi férié
|
||||
);
|
||||
|
||||
self::assertFalse(($this->handler)($query));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function vacationDayIsNotSchoolDay(): void
|
||||
{
|
||||
$this->seedCalendarWithVacation('2025-02-08', '2025-02-23', 'Vacances hiver');
|
||||
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-02-10', // Lundi en vacances
|
||||
);
|
||||
|
||||
self::assertFalse(($this->handler)($query));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function regularWeekdayWithCalendarIsSchoolDay(): void
|
||||
{
|
||||
$this->seedCalendarWithHoliday('2025-05-01', 'Fête du travail');
|
||||
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-05-02', // Vendredi normal
|
||||
);
|
||||
|
||||
self::assertTrue(($this->handler)($query));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRejectsMalformedDate(): void
|
||||
{
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: 'invalid-date',
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('La date doit être au format YYYY-MM-DD.');
|
||||
|
||||
($this->handler)($query);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRejectsImpossibleCalendarDate(): void
|
||||
{
|
||||
$query = new IsSchoolDayQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
date: '2025-02-30',
|
||||
);
|
||||
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('La date n\'existe pas dans le calendrier.');
|
||||
|
||||
($this->handler)($query);
|
||||
}
|
||||
|
||||
private function seedCalendarWithHoliday(string $date, string $label): void
|
||||
{
|
||||
$calendar = SchoolCalendar::initialiser(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
||||
);
|
||||
$calendar->ajouterEntree(new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::HOLIDAY,
|
||||
startDate: new DateTimeImmutable($date),
|
||||
endDate: new DateTimeImmutable($date),
|
||||
label: $label,
|
||||
));
|
||||
$this->repository->save($calendar);
|
||||
}
|
||||
|
||||
private function seedCalendarWithVacation(string $start, string $end, string $label): void
|
||||
{
|
||||
$calendar = SchoolCalendar::initialiser(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
||||
);
|
||||
$calendar->ajouterEntree(new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::VACATION,
|
||||
startDate: new DateTimeImmutable($start),
|
||||
endDate: new DateTimeImmutable($end),
|
||||
label: $label,
|
||||
));
|
||||
$this->repository->save($calendar);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Query\ValidateHomeworkDueDate;
|
||||
|
||||
use App\Administration\Application\Query\ValidateHomeworkDueDate\ValidateHomeworkDueDateHandler;
|
||||
use App\Administration\Application\Query\ValidateHomeworkDueDate\ValidateHomeworkDueDateQuery;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntry;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryId;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\CalendarEntryType;
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolCalendarRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ValidateHomeworkDueDateHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string ACADEMIC_YEAR_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemorySchoolCalendarRepository $repository;
|
||||
private ValidateHomeworkDueDateHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemorySchoolCalendarRepository();
|
||||
$this->handler = new ValidateHomeworkDueDateHandler(
|
||||
calendarRepository: $this->repository,
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function weekdayWithNoCalendarIsValid(): void
|
||||
{
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: '2025-03-10', // Lundi
|
||||
));
|
||||
|
||||
self::assertTrue($result->valid);
|
||||
self::assertNull($result->reason);
|
||||
self::assertSame([], $result->warnings);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function weekendIsInvalid(): void
|
||||
{
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: '2025-03-15', // Samedi
|
||||
));
|
||||
|
||||
self::assertFalse($result->valid);
|
||||
self::assertStringContainsString('weekend', $result->reason);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function holidayIsInvalid(): void
|
||||
{
|
||||
$this->seedCalendarWithEntries(
|
||||
new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::HOLIDAY,
|
||||
startDate: new DateTimeImmutable('2025-05-01'),
|
||||
endDate: new DateTimeImmutable('2025-05-01'),
|
||||
label: 'Fête du travail',
|
||||
),
|
||||
);
|
||||
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: '2025-05-01',
|
||||
));
|
||||
|
||||
self::assertFalse($result->valid);
|
||||
self::assertStringContainsString('férié', $result->reason);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function vacationDayIsInvalid(): void
|
||||
{
|
||||
$this->seedCalendarWithEntries(
|
||||
new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::VACATION,
|
||||
startDate: new DateTimeImmutable('2025-02-08'),
|
||||
endDate: new DateTimeImmutable('2025-02-23'),
|
||||
label: 'Vacances hiver',
|
||||
),
|
||||
);
|
||||
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: '2025-02-10', // Lundi en vacances
|
||||
));
|
||||
|
||||
self::assertFalse($result->valid);
|
||||
self::assertStringContainsString('vacances', $result->reason);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function returnDayFromVacationIsValidWithWarning(): void
|
||||
{
|
||||
$this->seedCalendarWithEntries(
|
||||
new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::VACATION,
|
||||
startDate: new DateTimeImmutable('2025-02-08'),
|
||||
endDate: new DateTimeImmutable('2025-02-23'),
|
||||
label: 'Vacances hiver',
|
||||
),
|
||||
);
|
||||
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: '2025-02-24', // Lundi retour de vacances
|
||||
));
|
||||
|
||||
self::assertTrue($result->valid);
|
||||
self::assertCount(1, $result->warnings);
|
||||
self::assertStringContainsString('retour de vacances', $result->warnings[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function normalSchoolDayIsValid(): void
|
||||
{
|
||||
$this->seedCalendarWithEntries(
|
||||
new CalendarEntry(
|
||||
id: CalendarEntryId::generate(),
|
||||
type: CalendarEntryType::HOLIDAY,
|
||||
startDate: new DateTimeImmutable('2025-05-01'),
|
||||
endDate: new DateTimeImmutable('2025-05-01'),
|
||||
label: 'Fête du travail',
|
||||
),
|
||||
);
|
||||
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: '2025-05-02', // Vendredi normal
|
||||
));
|
||||
|
||||
self::assertTrue($result->valid);
|
||||
self::assertSame([], $result->warnings);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function malformedDateIsInvalid(): void
|
||||
{
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: 'not-a-date',
|
||||
));
|
||||
|
||||
self::assertFalse($result->valid);
|
||||
self::assertStringContainsString('YYYY-MM-DD', $result->reason);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function impossibleCalendarDateIsInvalid(): void
|
||||
{
|
||||
$result = ($this->handler)(new ValidateHomeworkDueDateQuery(
|
||||
tenantId: self::TENANT_ID,
|
||||
academicYearId: self::ACADEMIC_YEAR_ID,
|
||||
dueDate: '2025-02-30',
|
||||
));
|
||||
|
||||
self::assertFalse($result->valid);
|
||||
self::assertStringContainsString('n\'existe pas', $result->reason);
|
||||
}
|
||||
|
||||
private function seedCalendarWithEntries(CalendarEntry ...$entries): void
|
||||
{
|
||||
$calendar = SchoolCalendar::initialiser(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID),
|
||||
);
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$calendar->ajouterEntree($entry);
|
||||
}
|
||||
|
||||
$this->repository->save($calendar);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user