feat: Gestion des périodes scolaires

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.
This commit is contained in:
2026-02-06 12:00:29 +01:00
parent 0d5a097c4c
commit f19d0ae3ef
69 changed files with 5201 additions and 121 deletions

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
use function sprintf;
final class InvalidPeriodCountException extends RuntimeException
{
public static function forType(string $type, int $expected, int $actual): self
{
return new self(sprintf(
'Le mode "%s" attend %d période(s), mais %d fournie(s).',
$type,
$expected,
$actual,
));
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
use function sprintf;
final class InvalidPeriodDatesException extends RuntimeException
{
public static function endBeforeStart(string $label, string $start, string $end): self
{
return new self(sprintf(
'La date de fin (%s) de la période "%s" doit être postérieure à la date de début (%s).',
$end,
$label,
$start,
));
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
use function sprintf;
final class PeriodeAvecNotesException extends RuntimeException
{
public static function confirmationRequise(string $label): self
{
return new self(sprintf(
'La période "%s" contient des notes. La modification peut impacter les bulletins existants. Confirmez avec confirmImpact=true.',
$label,
));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
use function sprintf;
final class PeriodeNonTrouveeException extends RuntimeException
{
public static function pourSequence(int $sequence, string $academicYearId): self
{
return new self(sprintf(
'Aucune période avec la séquence %d trouvée pour l\'année scolaire %s.',
$sequence,
$academicYearId,
));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
final class PeriodesDejaConfigureesException extends RuntimeException
{
public static function pourAnnee(string $academicYearId): self
{
return new self("Les périodes sont déjà configurées pour l'année scolaire {$academicYearId}.");
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
final class PeriodesNonConfigureesException extends RuntimeException
{
public static function pourAnnee(string $academicYearId): self
{
return new self("Aucune période configurée pour l'année scolaire {$academicYearId}.");
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
use function sprintf;
final class PeriodsCoverageGapException extends RuntimeException
{
public static function gapBetween(string $periodA, string $periodB): self
{
return new self(sprintf(
'Il y a un trou de couverture entre les périodes "%s" et "%s". Les périodes doivent être contiguës.',
$periodA,
$periodB,
));
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Administration\Domain\Exception;
use RuntimeException;
use function sprintf;
final class PeriodsOverlapException extends RuntimeException
{
public static function between(string $periodA, string $periodB): self
{
return new self(sprintf(
'Les périodes "%s" et "%s" se chevauchent.',
$periodA,
$periodB,
));
}
}