# Rollover Conges - Regles et Mise en Production

Document de reference pour expliquer le fonctionnement metier du report N-1 et preparer le lancement en production.

## 1) Objectif

Eviter les recalculs "depuis le debut du contrat" et fiabiliser les soldes.

Principe:
- le solde est stocké par exercice
- au changement d'exercice, on ouvre la nouvelle période avec un "solde d'ouverture" (report N-1)
- un indicateur de cloture (`contractPaidLeaveSettled`) permet de couper la continuité entre 2 contrats

## 2) Exercices metier

- `CDI` / `CDD` non forfait:
  - exercice: `1er juin` au `31 mai`
  - `year` = annee de fin d'exercice (ex: `2026` = 01/06/2025 -> 31/05/2026)
- `FORFAIT`:
  - exercice: `1er janvier` au `31 decembre`
  - `year` = annee civile
- `INTERIM`:
  - hors perimetre conges

## 3) Logique de compteurs

- `acquis`:
  - correspond au report N-1 (solde d'ouverture)
- `en cours d'acquisition`:
  - correspond aux droits generes sur l'exercice en cours
- `pris`:
  - non forfait: absences type `C` (conge)
  - forfait: toutes absences
- `restant`:
  - `acquis + en_cours - pris` (borne a 0 dans l'affichage)

## 4) Effet du "solde de tout compte"

Le champ de cloture `contractPaidLeaveSettled` est saisi lors de la fermeture d'une periode contrat.

- `false`:
  - continuite des droits entre contrats
- `true`:
  - pas de reprise des droits precedents
  - reset de continuite au lendemain de la date de cloture

## 5) Table cible

Table `employee_leave_balances` (une ligne par employe et exercice):
- `employee_id`
- `rule_code` (`CDI_CDD_NON_FORFAIT` ou `FORFAIT_218`)
- `year`
- `opening_days`
- `opening_saturdays`
- `accrued_days`
- `accrued_saturdays` (optionnel selon implementation)
- `taken_days`
- `taken_saturdays`
- `closing_days`
- `closing_saturdays`
- `is_locked`
- `created_at`, `updated_at`

Contrainte unique recommandee:
- `(employee_id, rule_code, year)`

Etat implementation:
- la table est creee
- le calcul de synthese conges lit en priorite `opening_days/opening_saturdays` de cette table quand une ligne existe pour `(employee, rule_code, year)`
- si aucune ligne n'existe, le calcul reste base sur le report dynamique N-1
- **le report dynamique (`LeaveBalanceComputationService::computeDynamicClosingForYear`, qui alimente le solde d'ouverture de l'exercice suivant) ancre lui aussi sur cette table** : pour chaque exercice de sa boucle, si une ligne bootstrap existe il part de `opening_days/opening_saturdays` (et ajoute l'offset `taken_days/taken_saturdays`) au lieu de recalculer depuis l'embauche. Sans cet ancrage, la clôture d'un exercice consulté en avance (ex. exercice suivant) cumulerait une année pleine d'acquisition par exercice antérieur à la mise en service — aucune absence historique n'étant saisie (cas Aurore : 88 jours au lieu de 31).
- la commande `app:leave:rollover` recalcule **toujours** le report via `computeDynamicClosingForYear(N-1)` (et ne se fie plus au `closing_days` stocké, qui n'est qu'un placeholder = `opening`), puis fige ce résultat dans le `closing_days` de l'exercice qui se termine ; voir § 6

### Definition des colonnes

- `employee_id`:
  - identifiant employe (FK vers `employees`)
  - une ligne de solde par employe / regle / exercice
- `rule_code`:
  - code de regle appliquee (`CDI_CDD_NON_FORFAIT`, `FORFAIT_218`)
  - permet de savoir quelles regles de calcul sont utilisees
- `year`:
  - annee d'exercice
  - non forfait: annee de fin d'exercice (`2026` = 01/06/2025 -> 31/05/2026)
  - forfait: annee civile (`2026` = 01/01/2026 -> 31/12/2026)
- `opening_days`:
  - report N-1 en jours (solde d'ouverture)
- `opening_saturdays`:
  - report N-1 "samedis" (0 pour forfait)
- `accrued_days`:
  - droits generes sur l'exercice courant (N)
- `accrued_saturdays`:
  - droits samedis generes sur N (0 pour forfait)
- `taken_days`:
  - jours poses sur l'exercice
- `taken_saturdays`:
  - samedis poses sur l'exercice (0 pour forfait)
- `closing_days`:
  - solde de cloture jours (`opening_days + accrued_days - taken_days`)
- `closing_saturdays`:
  - solde de cloture samedis (`opening_saturdays + accrued_saturdays - taken_saturdays`)
- `is_locked`:
  - `false` sur exercice ouvert (recalcul possible)
  - `true` apres validation RH (exercice fige)
- `created_at`, `updated_at`:
  - trace technique creation / mise a jour

## 6) Rollover automatique

Commande quotidienne (cron) idempotente.

- commande Symfony: `php bin/console app:leave:rollover`
- comportement date metier:
  - le `01/01`: traite uniquement `FORFAIT_218`
  - le `01/06`: traite uniquement `CDI_CDD_NON_FORFAIT`
  - les autres jours: sortie sans action
- option manuelle: `--force` pour executer hors date metier (reprise/correction)

Date d'effet:
- forfait: au `1er janvier`
- non forfait: au `1er juin`

Traitement par employe:
1. determiner le report de l'exercice precedent:
   - si cloture `paidLeaveSettled=true` sur la periode precedente => report `0`
   - sinon report = **cloture reelle recalculee** via `computeDynamicClosingForYear(exercicePrecedent)` (acquisition + samedis + fractionnes − pris, ancree sur l'`opening_days` bootstrap de chaque exercice). On **ne se fie PAS** au `closing_days` stocke : il n'est jamais recalcule apres creation (toujours egal a l'`opening`), donc s'y fier propagerait l'ouverture sans jamais crediter l'acquisition de l'annee (cas Aurore : report 0 au lieu de 31).
2. **figer** ce report dans `closing_days/closing_saturdays` de la ligne de l'exercice qui se termine (la colonne contient enfin un vrai solde de cloture, auditable).
3. creer la ligne du nouvel exercice avec ce report en `opening_*`
4. initialiser `accrued/taken/closing` pour le nouvel exercice (= `opening` a la creation)

### Correction manuelle d'un solde (RH / comptable)

Le verrouillage (`is_locked`) n'est pas utilise ; les corrections se font directement en BDD. Deux garde-fous rendent cela sur :

- **Idempotence** : le cron ne cree la ligne d'un exercice que si elle n'existe pas (les lignes existantes sont ignorees). Une ligne corrigee a la main n'est donc **jamais** ecrasee par un passage ulterieur du cron (meme avec `--force`).
- **Le bon levier est `opening_days`, pas `closing_days`** : `computeDynamicClosingForYear` part de l'`opening_days` de chaque exercice comme ancre. Corriger l'`opening_days` d'un exercice (ou la donnee de fond : absence, fractionne, paye) se propage automatiquement aux reports des exercices suivants. Editer un `closing_days` d'un exercice **pas encore bascule** est inutile (il sera recalcule a la bascule) ; une fois la ligne suivante creee, plus rien n'y touche.

## 7) Donnees a fournir au go-live

La RH doit fournir un import d'ouverture:

Colonnes minimales:
- `employee_identifier` (id interne ou matricule)
- `rule_code`
- `year`
- `opening_days`
- `opening_saturdays` (0 pour forfait)
- `source_date` (date de reference du relevé RH)
- `comment` (optionnel)

Format recommande:
- CSV UTF-8
- separateur `;`
- decimales en point (`7.5`)

Exemple:
```csv
employee_id;rule_code;year;opening_days;opening_saturdays;source_date;comment
42;CDI_CDD_NON_FORFAIT;2026;12.5;2;2026-05-31;Reprise fichier RH
17;FORFAIT_218;2026;8;0;2025-12-31;Reprise fichier RH
```

## 8) Checklist mise en prod

1. Valider le mapping employe RH -> employe applicatif
2. Importer les soldes d'ouverture N-1
3. Verifier 5 cas metier:
   - CDI simple sans changement de contrat
   - CDD -> CDI avec `paidLeaveSettled=false`
   - CDD -> CDI avec `paidLeaveSettled=true`
   - Forfait sur annee complete
   - Forfait avec debut en cours d'annee
4. Activer le cron de rollover
5. Geler (`is_locked`) les exercices historicises valides

Exemple cron (tous les jours a 02:10):
Dev
```cron
10 2 * * * cd /var/www/html && php bin/console app:leave:rollover --no-interaction 2>&1
```
Prod
```cron
10 2 * * * cd /var/www/sirh && php bin/console app:leave:rollover --no-interaction 2>&1
```
Explication de la ligne cron:
- `10 2 * * *`: planification
  - `10` = minute
  - `2` = heure
  - `*` = tous les jours du mois
  - `*` = tous les mois
  - `*` = tous les jours de la semaine
- `cd /var/www/html`: se place dans le dossier de l application Symfony
- `php bin/console app:leave:rollover --no-interaction`: execute le rollover sans demander de confirmation
  - hors `01/01` et `01/06`, la commande sort en no-op (normal)
- `>> var/log/leave-rollover.log`: ajoute la sortie standard dans le fichier de log (sans ecraser l historique)
- `2>&1`: redirige aussi les erreurs dans le meme fichier de log

Execution manuelle forcee:
```bash
php bin/console app:leave:rollover --force --no-interaction
```

Exemple de verification rapide:
```bash
tail -n 50 /var/www/html/var/log/leave-rollover.log
```

## 9) Points de vigilance

- Ne jamais recalculer les soldes historiques apres validation RH sans procedure explicite
- Garder une trace de toute correction manuelle (auteur, date, motif)
- Aligner strictement les regles UI et API sur les memes compteurs (pas de formule differente front/back)

## 10) Regle de consommation des droits

Regle metier:
- un employe peut poser des conges en cours d'acquisition
- la consommation se fait par ordre:
  1. `acquis` (report N-1)
  2. `en cours d'acquisition` (droits N)

Effet attendu:
- si `acquis = 0` et `en cours = 7.5`, puis prise de `7`, alors:
  - `acquis` reste `0`
  - `en cours` devient `0.5`
- si `acquis = 0` et `en cours = 2.5`, puis prise de `3`, alors:
  - `acquis` reste `0`
  - `en cours` devient `-0.5` (dette)
  - le mois suivant, une acquisition de `2.5` ramené `en cours` a `2.0`

Formule de lecture recommandée:
- `restant_acquis = max(0, acquis - pris)`
- `reste_a_imputer_sur_en_cours = max(0, pris - acquis)`
- `restant_en_cours = en_cours - reste_a_imputer_sur_en_cours` (valeur negative autorisee)
