8.3 KiB
Suspension de contrat — Design Spec
Objectif
Permettre de suspendre un contrat employé. Une suspension empêche l'acquisition de congés durant la période concernée (prorata). S'applique aux CDI/CDD non forfait et aux forfaits 218.
Contraintes
- Plusieurs suspensions possibles par période de contrat
- Pas de suppression de suspension (hors scope)
- Règle de calcul : on exclut les jours de suspension jusqu'au dernier mois complet terminé (cohérent avec la règle existante)
- Suspension sans date de fin = suspension en cours indéfiniment (exclut les mois jusqu'au dernier mois terminé)
- Les suspensions ne doivent pas se chevaucher sur une même période de contrat
Modèle de données
Nouvelle entité ContractSuspension
Nouvelle table contract_suspensions :
| Colonne | Type | Nullable | Description |
|---|---|---|---|
id |
SERIAL | non | PK |
contract_period_id |
INT | non | FK vers employee_contract_periods, CASCADE delete |
start_date |
DATE | non | Début de suspension |
end_date |
DATE | oui | Fin de suspension (null = en cours) |
comment |
TEXT | oui | Commentaire libre |
created_at |
TIMESTAMP | non | Date de création technique |
Index : (contract_period_id, start_date).
Relation : EmployeeContractPeriod ← OneToMany → ContractSuspension.
Backend — API
Endpoint dédié
Les suspensions sont gérées via un endpoint dédié plutôt que via les champs transients Employee. Cela évite de complexifier le EmployeeWriteProcessor et permet de gérer N suspensions proprement.
Nouvel ApiResource ContractSuspension :
POST /api/contract_suspensions— créer une suspension (body :contractPeriodIRI,startDate,endDate,comment)PATCH /api/contract_suspensions/{id}— modifier une suspension existante- Security :
ROLE_ADMIN - Pagination désactivée
Processor custom ContractSuspensionWriteProcessor :
- Résout la période de contrat depuis l'IRI
- Validation :
startDaterequisendDate >= startDatesi renseignéstartDate >= period.startDate- Pour les CDD/contrats avec date de fin :
startDateetendDatedans les bornes de la période - Pas de chevauchement avec les autres suspensions de la même période
- Rejet si la période de contrat est déjà clôturée (date de fin dans le passé)
Lecture
Exposer les suspensions dans la sérialisation de l'Employee :
Employee::getCurrentSuspensions(): array— retourne les suspensions de la période de contrat courante, groupeemployee:read- Ajouter les suspensions au
ContractHistoryItemviaEmployeeContractPeriod::getSuspensions()
Backend — Calcul des congés
Deux points d'impact
Le calcul d'acquisition existe à deux endroits qui doivent tous les deux prendre en compte les suspensions :
EmployeeLeaveSummaryProvider::computeAccruedDaysFromStart()— affichage live des congés en cours d'acquisitionLeaveBalanceComputationService::computeAccruedDays()— utilisé par le rollover (LeaveRolloverCommand) pour calculer le solde de report
Les deux méthodes ont la même structure (boucle mois par mois) et doivent être modifiées de la même manière.
Modification des méthodes de calcul
Pour les deux méthodes, ajouter un paramètre optionnel : array $suspensions = [] (tableau de {start: DateTimeImmutable, end: ?DateTimeImmutable}).
Dans la boucle mois par mois, pour chaque mois :
- Calculer les jours couverts par la période de contrat (existant)
- Pour chaque suspension, calculer le nombre de jours suspendus qui tombent dans ce mois
- Soustraire le total des jours suspendus
- Le ratio du mois = max(0, jours couverts - jours suspendus) / jours dans le mois
Cela gère automatiquement les suspensions qui commencent/finissent en milieu de mois (prorata).
Une suspension sans date de fin utilise la date de fin de calcul comme borne (dernier jour du mois précédent, cohérent avec la règle existante).
Note : chaque méthode est appelée deux fois — une pour les jours, une pour les samedis. La soustraction de suspension s'applique aux deux appels.
Impact sur les forfaits 218
Pour les forfaits, les jours acquis en début d'exercice (ex: 34 jours pour 2026) sont réduits au prorata des jours de suspension.
Calcul : jours acquis = base × (jours ouvrés effectifs / jours ouvrés totaux de l'exercice)
Où jours ouvrés effectifs = jours ouvrés totaux - jours ouvrés suspendus.
Cela impacte EmployeeLeaveSummaryProvider dans la branche forfait et LeaveBalanceComputationService dans le calcul forfait de computeDynamicClosingForYear().
Passage des données de suspension aux méthodes
EmployeeLeaveSummaryProvider: le provider a accès aux périodes de contrat via l'Employee. Il doit résoudre les suspensions de la période couvrant l'exercice et les passer aux méthodes de calcul.LeaveBalanceComputationService: le service utilise$employee->getContractHistory(). Il doit trouver les suspensions de la période couvrant l'exercice. L'accès au repositoryEmployeeContractPeriodRepositoryest déjà injecté — ajouter l'accès au repositoryContractSuspensionRepositoryou passer par la relation Doctrine.
Impact sur la bascule d'exercice (rollover au 01/06)
Le rollover (LeaveRolloverCommand) appelle LeaveBalanceComputationService::computeDynamicClosingForYear() qui appelle computeAccruedDays(). En modifiant computeAccruedDays() pour accepter et traiter les suspensions, le rollover prendra automatiquement en compte les suspensions. Les jours acquis au rollover reflèteront la déduction.
Exemple CDI : exercice 2027 (juin 2026 - mai 2027), 2 suspensions totalisant 3 mois → au lieu de 25j acquis, l'employé bascule avec ~18.75j (9 mois effectifs × 2.083j/mois).
Règles non impactées
- INTERIM : pas de congés gérés
Frontend — UI
Bouton et drawer
Le bouton "Clôturer" devient "Modifier". Il ouvre le drawer existant avec le titre "Modifier le contrat". Le bouton "+ Ajouter" (création de nouveau contrat) reste inchangé.
Le drawer contient 2 onglets :
Onglet "Clôturer" — contenu identique à l'actuel (type contrat, temps de travail, début contrat en readonly, date fin, commentaire, checkbox solde de tout compte).
Onglet "Suspendre" — formulaires empilés :
- Pour chaque suspension existante : un formulaire pré-rempli avec les 3 champs (date début, date fin, commentaire) et un bouton "Modifier"
- En bas : un bouton "+ Ajouter" qui ajoute un nouveau formulaire vide avec les 3 champs et un bouton "Ajouter"
- Chaque formulaire est indépendant (soumission individuelle)
Champs par formulaire de suspension
- Date de début (required, input date)
- Date de fin (optionnel, input date)
- Commentaire (optionnel, textarea)
Données nécessaires côté frontend
Nouveau type ContractSuspension (DTO) :
type ContractSuspension = {
id: number
startDate: string
endDate?: string | null
comment?: string | null
}
Ajouter au type Employee (DTO) :
currentSuspensions?: ContractSuspension[]
Ajouter au type ContractHistoryItem :
suspensions?: ContractSuspension[]
Nouveau service frontend/services/contractSuspensions.ts :
createSuspension(payload)— POSTupdateSuspension(id, payload)— PATCH
Exemples de calcul
CDI/CDD non forfait
Contrat CDI démarré le 01/06/2026, exercice 2027 (juin 2026 - mai 2027). Accrual : 25j / 12 mois = 2.083j/mois.
Sans suspension au 12/03/2027 (9 mois complets : juin-février) :
- En cours d'acquisition = 9 × 2.083 = 18.75j
Avec 2 suspensions (01/01 au 31/01 + 01/03 au 31/03 = 2 mois) au 12/04/2027 (10 mois complets - 2 suspendus = 8 mois effectifs) :
- En cours d'acquisition = 8 × 2.083 = 16.67j
Samedis (5/12 par mois) :
- Sans suspension : 9 × 0.417 = 3.75j
- Avec 2 suspensions : 8 × 0.417 = 3.33j
Forfait 218
Exercice 2026 (année civile), 34 jours acquis, 252 jours ouvrés dans l'année. Suspension de 2 mois (44 jours ouvrés).
- Jours ouvrés effectifs = 252 - 44 = 208
- Jours acquis = 34 × (208 / 252) = 28.06j
Hors scope
- Suppression d'une suspension
- Affichage de la suspension dans l'historique des contrats (les données sont sérialisées mais pas de rendu spécifique dans le tableau historique)
- Auto-fermeture des suspensions lors de la clôture du contrat