# Avancement — Lot 3 (Gestion administrative + Facturation, Modules 3 & 4 du cahier des charges v4.1)

*Même principe que `documentation/avancement-lot1.md` et
`documentation/avancement-lot2.md`. Points hors scope consolidés dans
`documentation/backlog-v2.md`.*

## §6 Module 3 — Gestion administrative & dossiers enfants

| § | Fonctionnalité | État | Détail |
| --- | --- | --- | --- |
| **a)** | **Inscriptions & admissions** | ✅ | Pré-inscription, liste d'attente, admission (crée Enfant + ResponsableLegal), refus motivé, vue capacité/occupation — 2026-08-03 |
| **b)** | **Fiche enfant complète** | ✅ | Identité éditable, responsables légaux (max 2), consentements image, contact médecin, gravité allergie, situations familiales sensibles avec validation Direction, documents administratifs — 2026-08-03 |
| c) | Personnes autorisées | ✅ | Fait au Lot 1/2 (`PersonneAutorisee`, validation Direction) |
| **d)** | **Contrats d'accueil** | ✅ | Jours/horaires, période, tarif convenu, historique des avenants — 2026-08-03 |
| **e)** | **Places, capacité & personnel** | ✅ | Création/édition de groupes, affectation du personnel, révocation/réactivation d'accès, accès temporaire borné — 2026-08-03 |
| **f)** | **Gestion RH & données** | ⚠️ | Archivage enfant, liste des dossiers, journal des actions — 2026-08-04. Fusion de doublons, import Excel, export complet, modèles de documents, suivi des notifications échouées : pas commencé |

## §7 Module 4 — Facturation & gestion financière

Pas commencé — bloqué sur ce premier incrément du Lot 3 (`Contrat`
maintenant disponible, mais `GrilleTarifaire`/`Facture` restent à
construire).

## Détail de b) Fiche enfant complète + d) Contrats d'accueil (livré le 2026-08-03)

Premier incrément du Lot 3, scopé à b) et d) uniquement (voir plan
approuvé). Décisions de scope notables :
- **Personne payeuse externe** (non-responsable légal) : hors scope, le
  modèle actuel (`estPersonnePayeuse` sur `EnfantResponsableLegal`)
  couvre le cas courant ; un vrai payeur tiers est plus naturel avec
  `Facture`/`GrilleTarifaire` (Module 4).
- **`AlerteEnfant` reste inchangée** (flag rapide, création libre) ;
  `SituationFamiliale` est un mécanisme **distinct et nouveau**, réservé
  aux 4 situations sensibles de §6.2.b (autorité exclusive, interdiction
  de remise, restriction de consultation/retrait, décision judiciaire),
  avec validation Direction obligatoire — les deux coexistent, pas de
  migration des alertes `RESTRICTION_FAMILIALE_JUDICIAIRE` existantes.
- **Documents administratifs** : réutilisation telle quelle de
  `StockagePhotoInterface`/`StockageLocalPhoto` (son contrat n'a rien de
  spécifique aux photos).
- **`ContratService::resilier()`** clôture le contrat sans calculer de
  solde — le calcul de solde au retrait en cours de contrat (§7.2.c)
  reste Module 4.

**Nouvelles entités** : `SituationFamiliale` (mirroring `PersonneAutorisee`,
réutilise l'enum `StatutAutorisation`), `DocumentAdministratif`, `Contrat`
(avenants = nouveau `Contrat` chaîné via `contratPrecedent`, désactivation
de l'ancien plutôt qu'entité `Avenant` séparée — même principe que
`AlerteEnfant`/`PersonneAutorisee`). Champs ajoutés : `Enfant.medecinNom`/
`medecinTelephone`, `AlerteEnfant.gravite` (pertinent pour `ALLERGIE`).

**Nouveaux Services** : `FicheEnfantAdminService` (identité, responsables
— règle des 2 max vérifiée en Service, `NombreResponsablesMaxAtteintException`
—, consentements, gravité allergie), `SituationFamilialeService`
(déclarer/valider/refuser/revoquer, journalisé), `DocumentAdministratifService`
(téléversement/suppression/lecture), `ContratService` (créerInitial/avenant/
résilier/historique, un seul contrat actif par enfant à la fois).

**Impact sur `PresenceService::restrictionActive()`** : étendu en OR avec
le chemin `SituationFamiliale` validée d'un type bloquant
(`INTERDICTION_REMISE`, `RESTRICTION_CONSULTATION_RETRAIT`,
`DECISION_JUDICIAIRE` — `AUTORITE_EXCLUSIVE` seule ne bloque pas une
sortie), sans remplacer le chemin `AlerteEnfant` historique.

**Sécurité "jamais visible côté parent"** : `SituationFamiliale` n'est
chargée par aucun Service/Contrôleur du parcours parent — absence de code
plutôt que filtre (contrairement au filtre explicite déjà en place sur
`AlerteEnfant`, cf. `avancement-lot2.md`).

**Nouveaux Contrôleurs** : `FicheEnfantAdminController`
(`/administration/enfant/{id}`) et `ContratController`
(`/administration/enfant/{id}/contrat`), tous deux sous
`^/administration -> ROLE_DIRECTION` (déjà posé au Lot 0, pas de nouvelle
règle `security.yaml`). Lien ajouté depuis l'écran équipe
(`equipe/fiche_enfant.html.twig`) vers la fiche admin (visible seulement
`ROLE_DIRECTION`), et affichage en lecture seule des situations
familiales actives sur ce même écran équipe. Écran parent
(`parents/presences_historique.html.twig`) étendu avec le planning prévu
au contrat actif, débloquant le ⚠️ d) documenté au Lot 2.

**Bug découvert et corrigé pendant la vérification manuelle** :
`Request::getInt()` lève une exception sur une chaîne vide (`FILTER_NULL_ON_FAILURE`
non positionné) plutôt que de la traiter comme absente — touchait
`groupe_id`, `responsable_id`, `document_id` (champs `<select>`
facultatifs). Corrigé par un helper `entierOptionnel()` qui lit la chaîne
et ne caste en `int` que si non vide.

**Erreur de template découverte et corrigée** : `contrat.html.twig` avait
un double `{% else %}` pour un seul `{% if %}` (erreur de syntaxe Twig
détectée seulement à l'exécution, pas par `php -l`). Corrigé en inversant
la condition du premier bloc.

Vérifié manuellement (serveur PHP local + curl, 3 rôles) : édition
identité + contact médecin ; ajout d'un 2e responsable (nouveau, créé à la
volée) ; refus du 3e avec message flash ; gravité d'allergie et
consentement mis à jour ; déclaration d'une situation familiale
`INTERDICTION_REMISE`, validation Direction ; **invisible sur les 3
routes parent testées** (profil, tableau de bord, présences) ; blocage
d'une autorisation ponctuelle et d'un pointage de départ pour cet enfant
via le nouveau chemin `SituationFamiliale` (confirme l'extension de
`restrictionActive()`) ; upload/téléchargement (contenu identique)/
suppression d'un document ; création d'un contrat, second contrat refusé
(couvert par le test unitaire, formulaire de création masqué une fois un
contrat actif), avenant (ancien désactivé, chaînage `contratPrecedent`
correct, historique conservé) ; le parent voit le planning **de
l'avenant**, pas de l'ancien contrat ; 403 pour un éducateur non-Direction
sur `/administration/enfant/{id}`.

`php bin/phpunit` : 78 tests, 162 assertions, tous verts (4 nouveaux
fichiers de tests + 1 test ajouté à `PresenceServiceTest` pour le nouveau
chemin de blocage). `doctrine:schema:validate` : mapping et base
synchronisés.

## Détail de a) Inscriptions & admissions (livré le 2026-08-03)

Deuxième incrément du Lot 3. Le cahier des charges (§6.2.a) est minimal
sur ce point (une seule puce, sans détail de workflow) — décisions de
scope notables :
- **`PreInscription` est une entité séparée**, pas un `Enfant`
  "brouillon" : un `Enfant` n'existe qu'une fois la pré-inscription
  admise. Évite de surcharger `Enfant.actif`/`dateSortie` (réservés à
  l'archivage en fin de présence, §6.2.f, cf. backlog f) avec une
  sémantique de statut d'admission différente.
- **Capacité de groupe = affichage informatif, pas de blocage dur** —
  contrairement à la règle dure des 2 responsables légaux max (§6.2.b),
  admettre un enfant dans un groupe déjà plein n'est pas empêché en
  Service : la capacité d'une crèche est négociable en pratique
  (dérogation, urgence), pas une limite structurelle des données.
- **Pas de déduplication automatique de responsable légal** entre une
  nouvelle pré-inscription et une famille déjà connue (fratrie) — un
  nouveau `ResponsableLegal` est toujours créé à l'admission. Le
  rapprochement/fusion de doublons est le sujet de f) "Gestion RH &
  données" (déjà au backlog), pas dupliqué ici.
- **Refus tracé uniquement dans `JournalAudit`**, pas de champ
  `motifRefus` sur l'entité — même convention que `PersonneAutorisee`.

**Nouvel enum** `StatutPreInscription` (EN_ATTENTE/LISTE_ATTENTE/ADMISE/
REFUSEE). **Nouvelle entité** `PreInscription` (identité enfant + contact
responsable en texte libre, `enfant` nullable posé à l'admission comme
lien d'audit). **Nouveau `InscriptionService`** :
`preinscrire()`/`mettreEnListeAttente()`/`admettre()`/`refuser()`, toutes
journalisées. `admettre()` crée `Enfant` + `ResponsableLegal` +
`EnfantResponsableLegal` dans le même geste (même schéma que
`FicheEnfantAdminService::creerEtAjouterResponsable()`, dupliqué plutôt
que forcé dans une méthode partagée). Nouvelle exception
`TransitionPreInscriptionInvalideException` (garde les transitions :
pas d'action sur une pré-inscription déjà admise/refusée).

**Nouveau `InscriptionController`** sous `/administration/inscriptions`
(ROLE_DIRECTION déjà garanti par `security.yaml`) — premier écran
"liste" du module, sur le modèle combiné de
`AdministrationMessagerieController`. Ajout d'une petite méthode
`GroupeRepository::compterEnfantsActifs()` pour la vue
capacité/occupation (§6.3). Liens de navigation ajoutés dans
`administration/_layout.html.twig` (Inscriptions, Messagerie) — jusque-là
chaque écran Direction n'était atteignable que par URL directe.

Vérifié manuellement (serveur PHP local + curl, 3 rôles) : création
d'une pré-inscription (visible dans "En attente") ; passage en liste
d'attente ; admission (groupe + date d'entrée + statut d'autorité
parentale) → nouvel `Enfant` créé avec son `ResponsableLegal` lié
(vérifié en base : `enfant_responsable_legal.est_personne_payeuse=1`,
`est_contact_urgence=1`), redirection directe vers sa fiche admin,
`PreInscription.statut=admise` et `enfant_id` correctement posés ; refus
motif vide bloqué avec flash d'erreur, refus avec motif fonctionnel,
apparaît dans l'historique avec lien vers la fiche ; vue
capacité/occupation cohérente avec le nombre d'enfants actifs en base
par groupe ; 403 pour un éducateur non-Direction.

`php bin/phpunit` : 86 tests, 184 assertions, tous verts (1 nouveau
fichier de tests, 8 cas). `doctrine:schema:validate` : mapping et base
synchronisés. `lint:twig`/`lint:container` : OK.

## Détail de e) Places, capacité & personnel (livré le 2026-08-03)

Troisième incrément du Lot 3. Particularité : **aucune migration** —
tout le modèle de données existait déjà depuis le Lot 0
(`Groupe.capaciteMax`/`ageMinMois`/`ageMaxMois`/`educateurs`,
`User.actif`/`dateExpirationAcces`, `AppUserChecker` déjà branché sur le
firewall pour bloquer un compte désactivé/expiré à la connexion) mais
rien ne le pilotait depuis un écran (`grep` confirmait zéro usage
d'`addEducateur`/`removeEducateur`/`setActif`/`dateExpirationAcces` en
dehors des entités et des fixtures).

Décision de scope notable : **la création de compte personnel (email/
mot de passe/rôle applicatif) reste hors périmètre.** §11 (matrice des
rôles) réserve "Comptes, rôles, configuration" à Admin technique, pas à
Direction (dont les acteurs Module 3, §6.1, sont "Direction,
secrétariat") — créer un compte avec un rôle applicatif est une décision
de sécurité différente d'assigner un compte existant à un groupe. Cet
incrément suppose donc le compte déjà créé (fixtures aujourd'hui) et
couvre les 3 actions du bullet §6.2.e sur des comptes existants :
affectation à un groupe, révocation/réactivation, accès borné dans le
temps. Réactivation ajoutée en plus du bullet (symétrique, pour corriger
une révocation par erreur).

**Nouveaux Services** : `GroupeService` (créer/modifier/activer/
désactiver un groupe), `PersonnelService` (affecter/retirer d'un groupe
— idempotent —, révoquer/réactiver l'accès, définir un accès temporaire
borné), toutes les mutations de `PersonnelService` journalisées.
**Nouveau `PersonnelController`** sous `/administration/personnel`
(ROLE_DIRECTION déjà garanti). Ajout de
`UserRepository::findPersonnel(Etablissement)` (personnel = comptes
rattachés à un établissement, ce qui exclut naturellement les comptes
Parent dont l'établissement est toujours null). Liens de navigation
ajoutés dans `administration/_layout.html.twig` (Inscriptions,
Personnel, Messagerie) — jusque-là chaque écran Direction n'était
atteignable que par URL directe.

Vérifié manuellement (serveur PHP local + curl, 3 rôles), avec un test
bout-en-bout réel de connexion (pas juste la base de données) : création
d'un groupe (capacité/tranche d'âge) ; affectation d'un éducateur à un
groupe puis retrait, vérifiés en base (`groupe_educateur`) et à l'écran ;
révocation d'un compte (`actif=0`) → tentative de connexion avec ce
compte refusée ; réactivation (`actif=1`) → connexion à nouveau réussie
(302 direct, contre l'échec précédent) ; accès borné à une date passée →
connexion refusée (redirection vers `/login` avec message générique
Symfony "Invalid credentials.", comportement par défaut du framework
pour ne pas distinguer les causes d'échec — pas modifié ici) ; effacement
de la date d'expiration → connexion à nouveau réussie ; 403 pour un
éducateur non-Direction sur `/administration/personnel`.

`php bin/phpunit` : 95 tests, 219 assertions, tous verts (2 nouveaux
fichiers de tests, 9 cas). `doctrine:schema:validate` : mapping et base
synchronisés (aucun changement de schéma, confirmé). `lint:twig`/
`lint:container` : OK.

## Détail de f) Gestion RH & données, premier incrément (livré le 2026-08-04)

Quatrième incrément du Lot 3. Le cahier des charges (§6.2.f) liste 6
sous-items hétérogènes en une phrase chacun ; cet incrément prend le
sous-ensemble le plus net (modèle de données déjà posé au Lot 0, jamais
exposé à l'écran — même logique que e)) :

- **Archivage d'un enfant en fin de présence** : `Enfant.actif`/
  `dateSortie` existaient déjà (Lot 0) et tous les écrans qui filtrent
  sur `actif` (équipe, photos, présences, fiche de secours...)
  fonctionnaient déjà correctement — confirmé, zéro régression.
  Manquait juste le bouton. Désarchivage symétrique ajouté (correction
  d'erreur, même logique que la réactivation d'accès personnel de
  l'incrément e). Volontairement **pas de cascade sur le contrat
  actif** : Direction résilie séparément via l'écran contrat existant
  si besoin — découple une action qui n'a pas de dépendance
  obligatoire (archiver un enfant sans contrat actif reste valide).
- **Liste des dossiers enfants** (écran de §6.3, manquant) : nécessaire
  pour que l'archivage soit utilisable en pratique, sans quoi Direction
  n'avait aucun moyen de naviguer vers la fiche d'un enfant hors du
  parcours équipe/admission.
- **Journal des actions** (dernier écran de §6.3 manquant) :
  `JournalAudit` était déjà écrit par tous les Services métiers depuis
  le Lot 0 mais jamais lu. Écran de consultation volontairement simple
  : les 200 entrées les plus récentes + un filtre texte exact optionnel
  sur l'entité concernée, pas de pagination ni de filtre par date/
  utilisateur (rester au strict "écran de consultation" du §6.3, sans
  sur-construire une UI de recherche non demandée).

**Nouvelles méthodes** `FicheEnfantAdminService::archiver()`/
`desarchiver()` (nécessitait d'injecter `JournalAuditService`, absent
jusque-là de ce Service). Nouvelles routes sur
`FicheEnfantAdminController` (`/archiver`, `/desarchiver`, même
convention que le reste du contrôleur). **Nouveaux `DossierEnfantController`**
(`/administration/enfants`) et **`JournalController`**
(`/administration/journal`), tous deux de purs écrans de lecture sans
Service intermédiaire (même convention que
`ContratController::afficher`/`AdministrationMessagerieController`).
Ajout de `EnfantRepository::findParEtablissement()` et
`JournalAuditRepository::findRecents()`. Liens de navigation ajoutés
dans `administration/_layout.html.twig` ("Dossiers enfants", "Journal").

**Aucune migration** : tout le modèle de données existait déjà.

Vérifié manuellement (serveur PHP local + curl, 3 rôles) : liste des
dossiers enfants ; un enfant visible sur l'écran équipe d'un groupe
disparaît immédiatement après archivage (`actif=0`/`dateSortie` posés
en base) et reste visible avec l'indicateur "archivé" dans la liste des
dossiers ; désarchivage → réapparaît côté équipe ; journal des actions
affiche les entrées d'archivage/désarchivage en tête, filtre
`?entite=Enfant` ne retourne que les 3 entrées `Enfant` de la session
(désarchivage, archivage, création à l'admission) ; 403 pour un
éducateur non-Direction sur les deux nouveaux écrans.

`php bin/phpunit` : 97 tests, 232 assertions, tous verts (2 nouveaux
cas sur `FicheEnfantAdminServiceTest`). `doctrine:schema:validate` :
mapping et base synchronisés, aucun changement de schéma confirmé.
`lint:twig`/`lint:container` : OK.

## Prochaine étape

Reste du Module 3 (f) : fusion de doublons (y compris entre une
nouvelle pré-inscription et une famille déjà connue, cf. décision de
scope de a)), import Excel initial, export complet, modèles de
documents, suivi des notifications échouées — chacun nécessitant une
vraie décision de conception ou une dépendance nouvelle, cf.
`documentation/backlog-v2.md`. Une fois ces points tranchés (ou
explicitement différés), le Module 3 sera complet. Module 4
(Facturation) entièrement à construire, débloqué par `Contrat` depuis
le premier incrément.