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:
2026-02-18 10:16:28 +01:00
parent 0951322d71
commit e06fd5424d
60 changed files with 7698 additions and 1 deletions

View File

@@ -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);
}
}