Files
SIRH/docs/superpowers/specs/2026-03-12-contract-suspension-design.md
tristan 38f09914cb
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
feat : ajout des suspensions et des jours de présence
2026-03-12 16:46:06 +01:00

8.3 KiB
Raw Blame History

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 : contractPeriod IRI, 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 :
    • startDate requis
    • endDate >= startDate si renseigné
    • startDate >= period.startDate
    • Pour les CDD/contrats avec date de fin : startDate et endDate dans 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, groupe employee:read
  • Ajouter les suspensions au ContractHistoryItem via EmployeeContractPeriod::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 :

  1. EmployeeLeaveSummaryProvider::computeAccruedDaysFromStart() — affichage live des congés en cours d'acquisition
  2. LeaveBalanceComputationService::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 :

  1. Calculer les jours couverts par la période de contrat (existant)
  2. Pour chaque suspension, calculer le nombre de jours suspendus qui tombent dans ce mois
  3. Soustraire le total des jours suspendus
  4. 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)

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 repository EmployeeContractPeriodRepository est déjà injecté — ajouter l'accès au repository ContractSuspensionRepository ou 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) — POST
  • updateSuspension(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