# Backlog v2 — points volontairement hors scope

*Idées et points laissés de côté sciemment pendant le Lot 1, pour ne pas les
oublier. Aucun n'est un bug ni un oubli : chacun a été tranché en
conscience pour garder chaque incrément simple, avec la raison notée
ci-dessous et une piste sur le moment logique pour y revenir (souvent un
Lot ultérieur, parfois juste "si le besoin se confirme").*

## Conformité — déclaration ARTCI (§2.2)

- **Déclaration/autorisation préalable auprès de l'ARTCI** pour le
  traitement des données de santé des enfants (allergies, traitements
  médicaux, PAI) — le cahier des charges (§2.2) signale explicitement
  ce point comme "à vérifier spécifiquement avant le lancement,
  idéalement avec un conseil local" (loi ivoirienne n° 2013-450, ARTCI
  = autorité de contrôle). **Jamais discuté avec le client, découvert
  le 2026-08-08** en relisant le cahier des charges dans son
  intégralité — aucun code à écrire ici, c'est une démarche
  juridique/administrative externe à l'application.
  → Transmis au client le 2026-08-08 (`documentation/points-en-attente.pdf`,
  avec deux autres points déjà connus : durée de conservation légale
  des factures — OHADA, voir Module 7 plus bas — et politique de
  sauvegarde des médias côté OVH). À trancher avant tout lancement en
  production avec des données de familles réelles.

## d) Présences & départs

- **"Proposer une personne autorisée permanente" côté parent** (§4.2.e) —
  nécessite l'espace Parents ; on part du principe que des personnes
  autorisées permanentes déjà validées peuvent exister en base sans ce
  flux. → Naturel avec le développement plus poussé de l'espace Parents.
- **Planning/horaires contractuels en base** (Lot 3) — l'heure prévue
  d'arrivée/départ est saisie manuellement faute de contrat structuré.
  → Lot 3, une fois `Contrat`/planning modélisés.
- **Vérification d'identité réelle à la sortie** — reste un geste humain de
  l'éducateur, non automatisée (pas de reconnaissance faciale etc.).
  → Pas prévu de l'automatiser.

## f) Messagerie avec les parents

- ~~Self-service de *création* de compte~~ (invitation par email,
  définition du mot de passe par la personne elle-même) — jusqu'ici
  Direction/Admin technique saisissait un mot de passe temporaire
  communiqué hors système. ~~Mot de passe oublié~~ — **fait** au Lot 2
  (2026-08-03), `ReinitialisationMotDePasseService`, générique à tous
  les rôles.
  → **Livré (2026-08-08)**, une fois Sweego configuré comme mailer réel
  (cf. section dédiée plus bas). Réutilise le mécanisme de jeton
  existant plutôt que d'en construire un nouveau : nouvelle méthode
  `ReinitialisationMotDePasseService::envoyerActivation(User)`, même
  entité `DemandeReinitialisationMotDePasse` (jeton à usage unique,
  seul son hash stocké), durée de validité propre (72h, contre 1h pour
  "mot de passe oublié" — une création de compte n'a pas la même
  urgence). Les 3 services de création (`CompteUtilisateurService`,
  `CompteInvestisseurService`, `CompteParentService`) génèrent
  désormais un mot de passe aléatoire jamais révélé (fonctionnellement
  équivalent à "pas de mot de passe" tant que le lien n'a pas été
  suivi) et déclenchent l'envoi ; les 3 formulaires de création perdent
  leur champ "mot de passe temporaire". Échec d'envoi tracé via
  `NotificationEchoueeService` (compte injoignable à sa création, plus
  critique qu'une notification best-effort) — sauf compte "transverse"
  Admin technique sans établissement, faute d'un établissement à qui
  rattacher l'échec. Le lien expiré/déjà utilisé a pour secours naturel
  "mot de passe oublié" (même mécanisme, déjà self-service).
  `CompteParentService` gagne au passage la journalisation de la
  création (`JournalAuditService`), absente jusqu'ici contrairement aux
  deux autres services. Vérifié en E2E sur la base dev réelle : compte
  créé sans mot de passe utilisable, lien d'activation fonctionnel
  (mot de passe défini, connexion réussie), jeton à usage unique
  confirmé (réutilisation du lien rejetée), échec d'envoi simulé
  (mailer cassé) correctement tracé et visible sur
  `/administration/notifications-echouees`.

## c) Photos et vidéos

- **Vidéos** — stockage/streaming demande transcodage + lecture par plages
  HTTP, un vrai nouveau chantier d'infra (ffmpeg typiquement).
  → À traiter séparément si le besoin se confirme, entité dédiée plutôt
  que de forcer `Photo` à couvrir les deux.
- ~~Antivirus/anti-malware à l'upload~~ — **risque accepté avec le
  client (2026-08-07)**, cf. section dédiée plus bas ("Antivirus à
  l'upload — risque accepté").
- **Quota de stockage par établissement** — la taille de chaque fichier est
  déjà enregistrée (`Photo.tailleOctets`), donc calculable ; il manque
  juste l'entité de configuration de la limite + le contrôle bloquant.
  → Petit chantier, à faire quand plusieurs établissements existeront
  réellement (actuellement un seul, cf. §12 "Multi-site").
- **Sélecteur graphique** (cliquer-glisser) pour choisir la zone à flouter/
  recadrer — V1 : 4 champs numériques (x/y/largeur/hauteur).
  → Amélioration ergonomique JS pure, non bloquante, peut venir à tout moment.
- **Correction automatique de l'orientation EXIF** (photos de téléphone).
  → Petit ajout GD/`exif_read_data`, pas fait en V1 par manque de temps
  plutôt que par choix de fond.
- ~~Consultation des photos côté parents~~ — **fait** au premier incrément
  du Lot 2 (2026-08-03), `EspaceParentController::photos()`.
- **Décision de sauvegarde des médias** (§2.5 : "inclus dans les
  sauvegardes, ou considérés remplaçables ?") — **discuté avec le
  client (2026-08-07), pas encore confirmé côté hébergement.**
  Recommandation : inclure les médias dans les sauvegardes — pas
  remplaçables (une photo/vidéo prise à un instant donné ne peut pas
  être régénérée, contrairement à une donnée ressaisissable), et pour
  éviter qu'une restauration ne laisse des références en base
  (`Photo`/`VideoEnfant`) pointant vers des fichiers absents si seule
  la base était sauvegardée. Reste purement une décision
  d'hébergement/exploitation, aucun code à écrire ici.
  → À vérifier concrètement côté OVH : le plan mutualisé sauvegarde-t-il
  automatiquement tout l'espace web (fichiers + base), ou seulement la
  base ? Point d'attention : les vidéos peuvent devenir volumineuses
  avec le temps si le plan a un quota de sauvegarde limité.

## h) Sécurité & santé

- ~~Nomenclature médicamenteuse structurée~~ — **confirmé avec le client
  (2026-08-07) : reste en texte libre.** `medicament`/`posologiePrevue`
  restent en texte libre sur `Traitement`, pas de base de données de
  médicaments (dosages standards, interactions...). Raison client :
  quel que soit le format (structuré ou texte libre), la donnée devra
  de toute façon être validée par un humain vu sa sensibilité — un
  référentiel structuré n'élimine pas cette validation, donc pas de
  gain suffisant pour justifier le chantier.
- ~~Gestion documentaire des ordonnances~~ — **confirmé avec le client
  (2026-08-07) : pas de besoin**, `Traitement.ordonnanceReference` reste
  un texte libre, pas de pièce jointe/scan de l'ordonnance.
- **Workflow de consentement électronique parental** pour l'autorisation
  d'un traitement — l'équipe trace ce qu'elle a reçu (`Traitement.autorisePar`),
  pas de validation en ligne par le parent lui-même via l'espace Parents.
  → Naturel une fois l'espace Parents plus développé (§4.2).

## i) Mode dégradé

- **Vraie synchronisation hors-ligne (PWA/service worker)** — le cahier des
  charges lui-même déconseille cette option ("plus simple à fiabiliser"
  sans elle, §5.2.i) ; la fiche de secours imprimable + le pointage
  provisoire papier + `PresenceService::corrigerPointage` suffisent au
  besoin exprimé.
  → À ne reconsidérer que si un vrai besoin terrain de saisie multi-écran
  simultanée hors-ligne apparaît (peu probable pour une crèche mono-site).
- ~~Export automatique quotidien programmé~~ de la fiche de secours —
  **confirmé avec le client (2026-08-07) : pas de besoin**, la
  génération à la demande (`window.print()`) suffit.

## Lot 2 — Espace Parents (suite du premier incrément, livré 2026-08-03)

Les deux premiers incréments ont couvert la consultation (b, c,
d-historique, i-lecture), la déclaration d'absence, et e)/g) (personnes
autorisées côté parent + notifications). Restent :

- ~~a) Mot de passe oublié~~ — **fait** au Lot 2 (2026-08-03), cf. entrée
  f plus haut.
- ~~h) Facturation et services optionnels~~ — **fait** au deuxième
  incrément du Module 4 (2026-08-04) : le parent consulte et télécharge
  ses factures (`EspaceParentController::factures()`/
  `telechargerFacture()`). Suivi des paiements/relances (§7.2.d) reste
  à construire séparément.
- ~~Modification de la fiche enfant soumise à validation~~ (i, côté
  parent) — le profil parent reste strictement en lecture seule ; ce qui a
  été livré au Lot 3 (2026-08-03) est l'édition **par Direction**
  (`FicheEnfantAdminController`), pas un workflow de demande initiée par
  le parent puis validée par la Direction.
  → **Confirmé avec le client (2026-08-07) : à construire** — workflow
  de demande de modification initiée par le parent, validée par
  Direction, distinct de l'édition directe existante. **Livré
  (2026-08-07)** : demande libre (texte décrivant la correction
  souhaitée, pas de champs structurés ni d'application automatique —
  Direction lit la demande, fait elle-même la modification via les
  écrans existants, puis marque la demande traitée/refusée). Nouvelle
  entité `DemandeModificationEnfant`, calquée sur `SituationFamiliale`
  (réutilise l'enum `StatutAutorisation`, aucun champ `motifRefus` —
  trace dans `JournalAudit`, notification email + suivi des échecs à
  la validation/au refus, même duo que
  `PersonneAutoriseeService::validerPermanente()`). Section dédiée sur
  `profil_enfant.html.twig` (parent) et `fiche_enfant_admin.html.twig`
  (Direction), badge "N en attente" sur la liste des dossiers enfants.
- **"Nouveau contenu" (g) au sens large** — g) a été scopé à 3 événements
  concrets (personne autorisée activée, incident signalé, nouvelle photo)
  plutôt qu'une notification à chaque repas/sieste/activité individuel
  (aurait été du spam).
  → À élargir seulement si un besoin précis se confirme (ex. digest
  quotidien plutôt qu'événement par événement).

## Lot 3 — Gestion administrative (premier incrément livré 2026-08-03)

Premier incrément (2026-08-03) : b) fiche enfant complète + d) contrats
d'accueil. Deuxième incrément (2026-08-03) : a) inscriptions &
admissions. Troisième incrément (2026-08-03) : e) places, capacité &
personnel. Quatrième incrément (2026-08-04) : f) archivage enfant +
liste des dossiers + journal des actions. Cf.
`documentation/avancement-lot3.md` pour le détail des quatre. Reste du
Module 3, tous dans f) "Gestion RH & données" :

- ~~Fusion de doublons~~ — **résolu par prévention (2026-08-07)**, cf.
  section dédiée plus bas ("Prévention des doublons ResponsableLegal").
  Pas de fusion a posteriori construite (jugée trop ambiguë) : la cause
  racine est corrigée à la source à l'admission et à l'ajout manuel
  d'un responsable.
- **Import initial Excel** — confirmé avec le client (2026-08-07) :
  usage unique (migration ponctuelle), pas récurrent. Aucune lib
  spreadsheet/CSV installée (`composer.json` vérifié).
  → Une commande CLI ponctuelle (comme `app:export:reversibilite`)
  suffira le jour d'une vraie migration, pas d'écran d'upload pérenne à
  construire.
- **Export complet** (Module 3, f) — **abandonné avec le client
  (2026-08-07)**, portée jugée trop floue (export par enfant ? base
  complète ? format CSV/PDF/Excel ?) pour être attaquée en l'état.
  L'export de réversibilité contractuel (§10) reste couvert séparément
  par `app:export:reversibilite`, déjà livré.
- **Modèles de documents** — **abandonné avec le client (2026-08-07)**,
  une seule ligne du cahier des charges sans élaboration (§6.2.f),
  aucun usage concret identifié.
- ~~Suivi des notifications échouées~~ — **arbitré et livré
  (2026-08-07)**, cf. section dédiée plus bas ("Suivi des notifications
  échouées"). Seules les notifications jugées importantes sont tracées
  dans un écran de suivi : sécurité/accès enfant (incident signalé,
  personne autorisée activée/refusée), relance de facture impayée,
  messagerie équipe↔parent. Photos/vidéos publiées et propositions
  entre parents restent en best-effort silencieux (niveau `warning`,
  comme avant — cf. `NotificationParentMailer`, `NotificationMessageMailer`).
- ~~Personne payeuse externe~~ (non-responsable légal, ex. employeur,
  grand-parent) — **confirmé avec le client (2026-08-07) : pas de
  nouvelle entité**, ces infos seront notées en texte libre (pas de
  modélisation structurée d'un tiers payeur). Le modèle actuel
  (`EnfantResponsableLegal.estPersonnePayeuse`) continue de ne couvrir
  que les responsables légaux eux-mêmes.
- **Création de compte personnel** — **traité (2026-08-06)**, cf.
  `documentation/specificites-hors-cahier-des-charges.md` : écran
  `/admin-technique/utilisateurs` (`AdminTechniqueController`,
  `CompteUtilisateurService`), réservé ROLE_ADMIN_TECHNIQUE conforme
  §11. e) (Direction) continue de ne couvrir que
  l'affectation à un groupe/la révocation/l'accès borné sur un compte
  déjà créé — répartition volontaire, pas un oubli.

## Module 4 — Facturation & gestion financière (clos, 2026-08-04/05)

a) Grille tarifaire, b) Génération des factures, d) Suivi des
paiements, c) Retrait en cours de contrat, i) Comptabilité générale
(export), f) Avances et apports du fondateur, h) Suivi de trésorerie,
j) Effectuer un remboursement, g) Cantine, j) Annuler une facture et
j) Modifier un paiement clôturé livrés (cf.
`documentation/avancement-lot4.md`) — **§7.2.j est complet (5/5)**.
e) acomptes/dépôt de garantie confirmé **sans objet** par le client
(2026-08-05) : la crèche facture un droit d'inscription normal, déjà
couvert par a)/b) (ligne `FORFAIT_UNIQUE`) — aucun mécanisme d'avance
ni de caution dans leur pratique réelle. **Module 4 entièrement
traité.**

- ~~**Repas cantine sans échéance de correction**~~ — **obsolète
  (2026-08-06)** : la cantine facturée aux parents (g) est passée
  d'un décompte de repas consommés à une enveloppe prépayée
  (`AbonnementCantine`), cf.
  `documentation/specificites-hors-cahier-des-charges.md`.
  `statutConsommationCantine` a été retiré, plus rien à corriger.

- ~~`DEPENSE_PERSONNELLE_A_REMBOURSER` jamais soldée~~ — un mouvement
  fondateur de cette catégorie reste une simple reconnaissance de dette
  (exclue du calcul de trésorerie, h) ; rien ne modélise le
  remboursement effectif au fondateur une fois cette dette réglée.
  → **Confirmé avec le client (2026-08-07) : à construire, et étendu**
  — pas seulement pour le fondateur : toute avance à rembourser, quelle
  que soit la personne (fondateur, directeur, autre membre de
  l'équipe...). Nécessite de revoir la portée du modèle actuel
  (`MouvementFondateur` est aujourd'hui spécifiquement pensé "fondateur",
  §7.2.f) — à cadrer avant de coder. **Livré (2026-08-08)** : décisions
  prises avec l'utilisateur — le champ bénéficiaire ne s'applique qu'aux
  deux catégories qui créent une dette (`avance_remboursable`,
  `depense_personnelle_a_rembourser`) ; le remboursement supporte des
  règlements multiples/partiels avec solde restant dû calculé
  automatiquement. `MouvementFondateur` gagne un champ
  `beneficiaire` (`User` nullable, obligatoire en Service pour ces deux
  catégories uniquement, peuplé depuis
  `UserRepository::findPersonnel()` — n'importe quel membre du
  personnel, plus seulement fondateur/direction) ; pas de renommage de
  l'entité/des routes (`journal_audit` historique reste cohérent).
  Nouvelle entité `RemboursementMouvementFondateur` (nommée à part de
  l'entité `Remboursement` existante, concept différent) : même moule
  double-intervenant que `Depense`/`MouvementFondateur`
  (`StatutValidationFinanciere`, `ValidationFinanciereGuard`, jamais de
  setter brut), un mouvement peut recevoir plusieurs remboursements
  jusqu'à solder le montant dû (rejeté si le total dépasserait le
  montant). Nouvel écran de détail par mouvement
  (`comptabilite_avances_detail`) affichant solde restant/historique et
  le formulaire de nouveau remboursement, lié depuis la liste
  DataTables existante (nouvelles colonnes Bénéficiaire/Solde dû).
  `TresorerieService` gagne une ligne sortants dédiée pour les
  remboursements validés (jusqu'ici aucun outflow ne reflétait ce
  règlement effectif). Vérifié en E2E sur la base dev réelle (avance
  avec bénéficiaire personnel, validation, deux remboursements
  partiels dont un soldant la dette, refus d'un remboursement avec
  motif obligatoire, rejet d'un remboursement dépassant le solde,
  impact confirmé sur `/comptabilite/tresorerie`).
- ~~Catégories de dépense non normalisées~~ (texte libre, h) —
  **confirmé avec le client (2026-08-07) : à construire**, liste
  fermée plutôt que texte libre. **Livré (2026-08-07)** : catalogue
  `CategorieDepense` configurable par établissement (même patron que
  `ThemeActivite` — création + activer/désactiver, jamais de suppression
  ni de renommage), gestion Direction via
  `/administration/categories-depense`. Seed de démarrage : Fournitures,
  Entretien, Cantine, Loyer, Travaux, Divers. Migration de bascule :
  chaque valeur `Depense.categorie` existante rattachée au catalogue
  (comparaison insensible à la casse), toute valeur non standard devenue
  sa propre catégorie (aucune perte de données). `CantineService`
  résout désormais sa catégorie système "Cantine" via
  `CategorieDepenseRepository::findParLibelle()`.

- ~~Attestations comptables~~ (§7.2.i, "attestations si nécessaire")
  — **abandonné avec le client (2026-08-07)**, même traitement que
  "modèles de documents" (Module 3, f) : une ligne du cahier des
  charges sans élaboration, pas d'usage concret identifié.
- ~~Export Excel natif (.xlsx)~~ — **confirmé avec le client
  (2026-08-07) : pas besoin pour l'instant**, le CSV suffit. Pourra
  évoluer si le besoin se précise (formules, mise en forme, plusieurs
  feuilles).

- ~~Statut "en retard" / échéance de paiement~~ — **confirmé avec le
  client (2026-08-07) : pas de délai standard à appliquer**, reste
  impayée/partiellement payée/payée uniquement.
- ~~Correction/suppression d'un paiement mal saisi~~ — **fait** au
  onzième incrément du Module 4 (2026-08-05), `CorrectionPaiement`
  (§7.2.j, "modifier un paiement clôturé") : écriture corrective
  signée, saisie Comptabilité/Direction, validation Direction.
  `Paiement` reste append-only, jamais modifié directement.
- ~~Exécution du remboursement proposé par c)~~ — **fait** au huitième
  incrément du Module 4 (2026-08-05), `RemboursementService`/
  `Remboursement` (§7.2.j, "effectuer un remboursement") : saisie
  Comptabilité/Direction, validation Direction, rattaché au `Contrat`
  et intégré à la section "Solde de retrait" de l'écran facture
  (`RetraitService`, c). `Paiement` reste positif uniquement — le
  remboursement est une entité séparée, pas une transaction négative.

## Module 5 — Pilotage / Reporting (livré 2026-08-05)

Tableau de bord (effectif/taux d'occupation par groupe, reporting RH
basique) et statistiques (inscriptions, chiffre d'affaires, impayés +
export CSV) livrés en un seul incrément — pur agrégateur en lecture
sur des données déjà modélisées, aucune nouvelle entité (cf.
`documentation/avancement-lot5.md`).

- **Reporting RH limité aux données présentes** (effectif par rôle,
  actifs/révoqués, accès temporaires expirés) — le cahier des charges
  ne prévoit aucune substance RH (salaire, contrat de travail,
  absences) et aucune n'existe dans le modèle de données actuel.
  → Un vrai module RH (paie, absences, contrats de travail) est un
  chantier à part entière, hors scope du cahier des charges v4.1.
- ~~Export CSV limité aux impayés~~ — chiffre d'affaires (un seul
  nombre) et inscriptions (quelques compteurs) affichés à l'écran
  uniquement, pas exportés en CSV. **Discuté avec le client
  (2026-08-07) : pas cette solution.** À la place, un nouveau **profil
  "investisseur"** est prévu — un rôle de consultation dédié montrant
  le chiffre d'affaires, le nombre d'enfants et d'autres informations
  utiles à la décision d'un investisseur, plutôt qu'un export CSV brut
  des compteurs existants.
  → À cadrer (quel périmètre exact de données visibles, quel modèle de
  rôle/permission, comment ces comptes sont créés) avant de coder —
  nouveau rôle applicatif, comparable en ampleur à l'ajout de
  ROLE_ADMIN_TECHNIQUE. **Livré (2026-08-08)** : décisions prises avec
  l'utilisateur — vue synthétique réduite (agrégats uniquement, pas de
  détail nominatif type liste d'impayés par famille) ; création de
  compte réservée à Direction (pas Admin technique, vu la sensibilité
  d'un compte détenu par un tiers externe à l'équipe). Nouveau rôle
  `ROLE_INVESTISSEUR` (`User::$roles`, simple chaîne dans la colonne
  JSON existante, aucune migration nécessaire — comme `ROLE_CANTINE`
  avant lui), nouvelle entrée `access_control` autonome
  (`^/investisseur`, pas de hiérarchie avec les autres rôles, même
  principe que `ROLE_ADMIN_TECHNIQUE`). `CompteInvestisseurService`
  (création, volontairement séparé de `CompteUtilisateurService` :
  un seul rôle possible, toujours rattaché à l'établissement de la
  Direction créatrice, pas de sélecteur) + `InvestisseurAdminController`
  sous `/administration/investisseurs` (Direction). Écran de
  consultation unique (`InvestisseurController`, `/investisseur`) fed
  par `TableauDeBordInvestisseurService`, pur agrégateur qui sélectionne
  4 champs (chiffre d'affaires, enfants actifs, taux d'occupation,
  solde de trésorerie) parmi ce que renvoient déjà
  `StatistiquesService`/`TableauDeBordService`/`TresorerieService` —
  aucune nouvelle requête SQL, aucune nouvelle entité. Vérifié en E2E
  sur la base dev réelle (création de compte par Direction, connexion
  investisseur, les 4 chiffres affichés confirmés identiques aux écrans
  Direction équivalents `/pilotage`/`/pilotage/statistiques`/
  `/comptabilite/tresorerie`, accès refusé — 403 — à toutes les autres
  zones y compris `/administration/investisseurs` lui-même, aucune
  donnée nominative sur l'écran, email déjà utilisé rejeté, traçabilité
  confirmée dans `journal_audit`).

## Module 6 — Communication générale (livré 2026-08-05)

Annonces générales, calendrier des événements et sondages simples
livrés en un seul incrément (cf.
`documentation/avancement-lot6.md`) — diffusion Direction/équipe vers
tous les parents de l'établissement.

- ~~Pas d'édition ni de suppression des annonces/événements/sondages~~
  (append-only, comme `TransmissionEquipe`) — **confirmé avec le
  client (2026-08-07) : pas de besoin en principe**, reste append-only.
- ~~Sondages toujours ouverts~~ (pas de date de clôture) — **confirmé
  avec le client (2026-08-07) : à construire.** Un sondage doit être
  limité à une durée, avec possibilité de modifier la date de fin (ou
  le nombre de jours) après coup. **Livré (2026-08-07)** :
  `Sondage.dateCloture` (non nul), calculée à la création à partir d'une
  durée en jours saisie par Direction/équipe, modifiable après coup via
  `/equipe/sondages/{id}/date-cloture` (jamais un setter nu — même
  convention que `AbonnementCantine::resilier()`). `estOuvert()` dérivé
  (comparaison à l'instant présent), aucune colonne de statut à
  maintenir. `voter()` rejette tout vote sur un sondage clôturé. Le
  sondage existant en base a reçu une clôture à horizon 10 ans lors de
  la migration, pour préserver son comportement "toujours ouvert"
  jusqu'à modification manuelle par Direction.
- **Pas d'accusé de lecture** sur les annonces/événements (contrairement
  à `Message`, qui a une notion de "prise en charge") — une annonce
  générale n'a pas de destinataire unique responsable d'y répondre.
  → Hors scope, cohérent avec la nature diffusion (pas de suivi
  individuel demandé par §9).

## Module 7 — Exigences d'exploitation (§10, évalué le 2026-08-05)

Endpoint de santé applicative et purge/rétention automatique livrés
(cf. `documentation/decisions-a-prendre.md`, section Module 7, pour
l'évaluation complète ligne par ligne et les arbitrages).

- **Journal d'audit (`JournalAudit`) exclu de la purge** — conçu dès
  le départ comme non-supprimable pour l'intégrité de la piste
  d'audit ; le §2.4 demandait une suppression après ~1 an mais
  l'invariant existant a été jugé prioritaire (arbitré avec
  l'utilisateur).
  → À revoir uniquement si un besoin réel de purge du journal émerge,
  probablement via un mécanisme d'archivage séparé plutôt qu'une
  suppression pure.
- **Factures exclues de la purge** — le §2.4 ne donne aucune durée
  chiffrée ("durée légale locale", non définie). **Discuté avec le
  client (2026-08-07), pas encore confirmé.** Piste (confiance
  modérée, à valider par l'expert-comptable du client, pas une
  affirmation juridique de notre part) : la Côte d'Ivoire relève du
  droit comptable OHADA (Acte uniforme relatif au droit comptable et à
  l'information financière), qui fixe généralement une conservation
  des documents comptables autour de 10 ans — mais une erreur dans un
  sens (purger trop tôt) a des conséquences légales réelles, donc pas
  de chiffre codé en dur sans confirmation. Le comportement actuel
  (factures jamais purgées automatiquement) reste le choix le plus sûr
  en attendant : mieux vaut trop conserver que purger par erreur.
  → À ajouter à `PurgeService`/`app:purge:donnees` une fois la durée
  confirmée par l'expert-comptable du client — même modèle que les
  règles déjà en place (6 mois suivi quotidien/photos, 1 an dossier
  administratif/messagerie).
- **Pas de planification automatique** (cron/systemd timer) de
  `app:purge:donnees` — infra/exploitation, hors scope de ce dépôt.
  → À planifier côté hébergeur/exploitant, ex. mensuel, avec
  `--force`.
- **`app:export:reversibilite` livré** (2026-08-05) — export complet
  de toute la base (44 entités), voir détail dans
  `documentation/decisions-a-prendre.md`, Module 7. **Module 7
  entièrement clos.**
- **Export ciblé (dossier d'un enfant, portabilité RGPD-like) et
  export CSV opérationnel (liste des dossiers)** — les deux autres
  usages identifiés dans la section "Export complet" (Module 3, f)
  restent non traités ; `app:export:reversibilite` couvre uniquement
  la réversibilité contractuelle (§10), pas ces deux usages métier.
  → À construire séparément si un besoin réel se confirme (écran
  Direction pour la liste CSV ; écran parent ou action Direction pour
  l'export par enfant).

## Suivi quotidien (§5.2.b) — enrichi le 2026-08-06

Comparaison client entre `documentation/suivi_enfants` (sa vision en 7
catégories) et l'implémentation existante, motivée par deux
préoccupations : des tierces personnes ("servantes") récupèrent
parfois les enfants et peuvent perdre des informations orales, et une
question sur une purge à 2 semaines (vérifiée : elle n'existe pas —
suivi quotidien et photos sont purgés 6 mois après le **départ** de
l'enfant, jamais pendant sa présence).

Livré : `Repas.particularite`/`Soin.particularite` (distinct du
commentaire, pour ressortir visuellement), compteur de changes du jour
mis en avant, catalogue `ThemeActivite` (Direction) + `Activite.theme`/
`participation`, `NoteHumeur.relationEnfants`/`relationAdultes`
(socialisation), nouvelle entité `SyntheseJournaliere` (commentaires de
l'éducatrice) avec validation Direction obligatoire avant diffusion
parent (réutilise `StatutAutorisation`), écrans équipe et parent
réorganisés en onglets par catégorie (`documentation/tab.png` fourni
comme référence visuelle par le client) pour que la personne qui
consulte — parent ou tierce personne — retrouve vite l'info sans être
noyée. Vidéo dans la galerie photos volontairement exclue (chantier
d'infrastructure distinct).

- **Pas d'édition/suppression de thème d'activité** (seulement
  créer/activer/désactiver) — cohérent avec le minimalisme déjà en
  place pour la gestion des groupes.
  → À ajouter si un thème créé par erreur doit être corrigé plutôt que
  désactivé.
- **`participation` absente de la saisie groupée d'activité**
  (individuel par nature, comme `Repas.commentaire` n'est pas collecté
  en saisie groupée) — reste à `null` pour les activités créées en
  groupe, correctible uniquement en resaisissant individuellement (pas
  d'écran d'édition a posteriori pour `Activite`).
  → Cohérent avec l'absence générale d'édition a posteriori sur le
  suivi quotidien (§2.3, modifications tracées via re-saisie, pas via
  update direct).

## Sélecteur d'enfant + vidéos (2026-08-06)

Suite à la discussion sur le suivi quotidien : deux ajouts distincts.

**Sélecteur d'enfant (espace parent)** : le nom d'enfant dans la barre
latérale (`templates/parents/_layout.html.twig`) devient un menu
déroulant (même composant KTMenu que le menu utilisateur) listant tous
les enfants du parent connecté, pour basculer directement d'un enfant à
l'autre sans repasser par "Mes enfants". Nouvelle fonction Twig
`mes_enfants()` (`src/Twig/ParentsExtension.php`) plutôt qu'une
variable transmise par chaque contrôleur : la barre latérale est
partagée par une dizaine d'écrans dans 5 contrôleurs différents.

**Vidéos** (documentation/suivi_enfants, point 7 "Gallerie photos —
image de l'enfant — video de l'enfant") : téléchargement uniquement
(2026-08-06, décision client — hébergement mutualisé OVH, pas de
ffmpeg possible pour générer une vignette). Nouvelle entité
`VideoEnfant`, distincte de `Photo` (qui est bâtie autour du
traitement d'image GD, sans rapport avec une vidéo) mais réutilisant
tout le reste : stockage, lien signé, consentement, notification
parent. `Content-Disposition: attachment` (contrairement à `inline`
pour les photos) : pas de lecture dans le navigateur. Taille max
configurable via `VIDEO_TAILLE_MAX_MO` (`.env`, 20 par défaut) — à
ajuster selon les limites réelles `upload_max_filesize`/`post_max_size`
d'OVH une fois vérifiées côté client.

- **Bug corrigé en cours de route** : `StockagePhotoInterface::genererLienTemporaire()`
  avait le nom de route `photos_telecharger` en dur — un lien vidéo
  généré via cette méthode partageait la même route que les photos,
  donc pointait vers le mauvais contrôleur (404 silencieux). Corrigé en
  ajoutant un paramètre `$nomRoute` explicite, chaque Service (Photo,
  Video) passe désormais le sien.
- **Purge/rétention** : vidéos ajoutées à `PurgeService`/`PurgeDonneesCommand`,
  même politique que les photos (6 mois après le départ de tous les
  enfants tagués).

## Solde cantine (2026-08-06)

Question client : le surplus de l'enveloppe cantine (recettes des
familles moins coût réel des ingrédients) finance parfois d'autres
dépenses de la crèche — comment le suivre proprement ? Réponse : pas de
mécanique comptable spéciale nécessaire (une seule trésorerie, un
forfait de service comme la scolarité, pas un remboursement de frais
réels à réconcilier ligne à ligne) — juste de la visibilité.

Livré : `AbonnementCantineService::solde()` (recettes = somme de tous
les `AbonnementCantine.montant` de l'établissement, actifs ou résiliés
— une facture déjà émise reste une recette réelle ; dépenses = somme
des `Depense` validées catégorie "Cantine"), affiché en carte
permanente sur `/comptabilite/depenses` (mis à jour à chaque
consultation). Volontairement **global**, pas par enfant : les achats
d'ingrédients sont communs à tous les enfants qui mangent à la cantine,
aucune dépense n'est attribuable à l'enveloppe d'un enfant en
particulier (confirmé avec le client). Objectif à terme : suivre ce
solde dans la durée pour ajuster le tarif "par repas" (`GrilleTarifaire`)
si le coût réel dérive de ce qui est collecté.

- ~~Pas de détail par période~~ (mois par mois) — **reconfirmé avec le
  client (2026-08-07) : pas de besoin**, solde depuis le début
  uniquement (voir juste l'écran Dépenses existant plutôt qu'un
  nouveau rapport).

## Auto-validation financière pour les comptes fondateurs (§7.2.j) (2026-08-06)

Demande client : le compte "qui peut tout faire" (Direction + Admin
technique cumulés — même profil que le compte fondateur testé lors de
la section "Sélecteur d'enfant + vidéos" ci-dessus) doit pouvoir
valider lui-même les opérations financières qu'il a saisies, sans
attendre une seconde personne. C'est explicitement permis
par le cahier des charges (§7.2.j, "précision sur la validation à
effectif réduit") : une auto-validation horodatée est admise quand une
seule personne habilitée est disponible, à condition qu'elle apparaisse
dans un rapport distinct soumis à un contrôle périodique.

**Découverte pendant l'investigation, confirmée avec le client** : sur
les 5 actions listées au §7.2.j (dépense, avance/apport du fondateur,
remboursement, annulation de facture, correction d'un paiement clôturé),
seule `DepenseService` bloquait déjà l'auto-validation. Les 4 autres
(`MouvementFondateurService`, `RemboursementService`,
`AvoirFactureService`, `CorrectionPaiementService`) ne vérifiaient que
le rôle `ROLE_DIRECTION` côté Contrôleur — n'importe quel compte
Direction pouvait donc déjà s'auto-valider dessus, silencieusement.
Décision client : durcir les 4 pour être cohérent avec les dépenses,
avec l'exception fondateur partout. Conséquence assumée : un compte
Direction qui n'est pas aussi fondateur a désormais besoin d'une
seconde personne pour ces 4 actions (déjà le cas pour les dépenses).

Livré :
- `User::peutAutoValiderFinancier()` — cumul `ROLE_DIRECTION` +
  `ROLE_ADMIN_TECHNIQUE` sur le même compte, identification du compte
  fondateur (décision client, aucun champ ni migration nécessaire).
- `ValidationFinanciereGuard` (nouveau service) — point de vérité
  unique de la règle "personne différente, sauf fondateur", partagé
  par les 5 services financiers plutôt que dupliqué cinq fois :
  contrairement aux petites duplications déjà assumées ailleurs dans ce
  projet, c'est une règle de conformité qui doit rester identique
  partout et ne pas diverger avec le temps.
- Les 5 services (`DepenseService`, `MouvementFondateurService`,
  `RemboursementService`, `AvoirFactureService`,
  `CorrectionPaiementService`) appellent le Guard en tête de
  `valider()`/`refuser()` et journalisent `auto_validation: true|false`
  dans `JournalAudit.details`.
- **"Rapport distinct" (§7.2.j)** : réutilise l'écran Journal existant
  (`/administration/journal`, réservé Direction) plutôt qu'un nouvel
  écran — `auto_validation: true` apparaît déjà dans la colonne
  "Détails" (JSON déjà affiché brut).
  → Si le besoin de repérer rapidement les auto-validations parmi de
  nombreuses entrées se confirme à l'usage, un filtre dédié sur cet
  écran serait la suite logique.

Vérifié manuellement : un compte fondateur saisit une dépense puis se
valide lui-même → succès, `auto_validation: true` dans le journal ; un
compte Direction seul (sans Admin technique) reste bloqué sur les
dépenses (comportement inchangé) et est désormais bloqué aussi sur une
avance du fondateur (nouveau comportement, permissif auparavant).

## Traçabilité comptable des dépenses : TVA, reçu vendeur informel, export annuel (2026-08-06)

Discussion client sur la préparation du bilan de fin d'année : le
comptable a besoin de preuves (photos de reçus) exportables, et la
Côte d'Ivoire mélange vendeurs avec facture normalisée (TVA 18%) et
vendeurs informels sans facture. Décisions actées avec le client :
garder un format "assujetti TVA" par défaut (TVA à 0 si l'établissement
n'est finalement pas assujetti, plutôt que deux branches logiques
séparées) ; un modèle de reçu à faire signer aux vendeurs informels,
avec des champs **vides** à remplir à la main (le vendeur n'étant pas
connu à l'avance, rien n'est pré-rempli par l'appli) ; stockage à plat
existant + export à la demande plutôt qu'une arborescence de dossiers
par mois.

**Décision de conception** : la pièce justificative reste optionnelle
sur `Depense` (pas rendue obligatoire). Le profil Cantine
(`CantineService`, achats au marché) répond à un besoin explicitement
voulu "simple, non fastidieux" sur téléphone, avec de nombreuses
petites lignes achetées à des vendeurs différents — obtenir un reçu
signé par ligne n'y est pas réaliste. Le modèle de reçu et la rigueur
TVA visent les achats identifiables (fournitures, travaux), pas les
courses au marché.

Livré :
- `Depense.montantHt`/`Depense.tauxTva` (nullable) : renseignés
  uniquement si le vendeur fournit une facture normalisée. `montant`
  reste le TTC, inchangé partout ailleurs (trésorerie, solde cantine).
  Le montant de TVA n'est jamais stocké séparément (dérivé à
  l'affichage : `montant - montantHt`) pour éviter une valeur qui
  diverge de ses composantes. `DepenseService::saisir()` exige les deux
  champs ensemble ou aucun, `montantHt <= montant`.
- Modèle de reçu vierge (`GET /comptabilite/export/modele-recu.pdf`,
  généré via Dompdf comme les factures) : à imprimer, faire remplir et
  signer par le vendeur sur place, puis photographier — devient la
  pièce justificative via le circuit `Depense` existant (aucun nouveau
  mécanisme d'upload).
- Export CSV des dépenses (`GET /comptabilite/export/depenses.csv`,
  filtrable par période) — même pattern que les exports
  factures/paiements déjà en place.
- Export ZIP annuel (`GET /comptabilite/export/depenses.zip`) :
  `recapitulatif.csv` + toutes les pièces justificatives disponibles
  sous `pieces/<id>.<extension>`, générés à la demande plutôt qu'une
  arborescence de fichiers à maintenir en continu.

- **Pas de calcul de déclaration TVA** — l'appli trace la décomposition
  HT/TVA/TTC par dépense pour le comptable, elle ne produit pas de
  déclaration fiscale.
  → Si le statut d'assujettissement de la crèche est confirmé plus
  tard et qu'un besoin de déclaration périodique émerge, une
  agrégation par période sur `montantHt`/`tauxTva` serait la suite
  logique.

## Pagination back-end des listes — socle générique + 2 écrans (2026-08-07)

Les listes chargeaient l'historique complet en une requête, sans
limite — `documentation/ameliorations-performance.md` documentait déjà
ce risque (factures/paiements en tête, non purgés, croissance
illimitée) et `JournalController` portait un TODO explicite ("pas de
pagination en v1"). Décision client : DataTables.net (jQuery, déjà
présent dans le bundle Metronic — confirmé par un test réel avant
implémentation) plutôt que KnpPaginatorBundle, pour éviter toute
dépendance Composer supplémentaire côté PHP. Contrainte explicite du
client : aucune dépendance à DataTables dans les services/repositories
— le protocole DataTables (`draw`, `start`/`length`, `order[0][column]`,
`search[value]`, la forme `recordsTotal`/`recordsFiltered`/`data`)
reste confiné à `App\Pagination\DataTablesAdapter`, seule classe du
projet qui le connaît.

Livré :
- Socle générique `src/Pagination/` : `CriteresPagination` (offset,
  limite, tri, recherche — concepts génériques, pas de notion
  DataTables), `ResultatPagine` (items + total filtré + total non
  filtré), `DataTablesAdapter` (traduction requête↔JSON, plafonne la
  taille de page demandée à 100, recase `draw` en `int` — recommandation
  officielle DataTables contre le XSS réfléchi).
- Repositories : `JournalAuditRepository::findRecentsPagine()` et
  `DepenseRepository::findParEtablissementPagine()`, en complément des
  méthodes non paginées existantes (gardées telles quelles pour les
  exports/trésorerie qui ont besoin de tout l'historique). Tri
  whitelisté en interne (jamais d'interpolation d'un nom de colonne
  venant de la requête) ; recherche libre sur Dépenses via `LIKE`
  paramétré (catégorie/description) ; Journal garde son filtre exact
  "entité" séparé, pas de recherche libre sur cet écran.
- `/administration/journal` et `/comptabilite/depenses` : la route
  d'affichage ne rend plus qu'une coquille, une nouvelle route
  `.../donnees` (JSON) sert les données. Sur Dépenses, les boutons
  Valider/Refuser (et leurs jetons CSRF, mêmes intentions qu'avant :
  `depenses_valider_<id>`/`depenses_refuser_<id>`) sont désormais
  générés côté contrôleur dans le payload JSON puis injectés en HTML
  côté client.
- DataTables 2.1.8 + styling Bootstrap 5 vendorés localement sous
  `public/assets/plugins/custom/datatables/` (pas de CDN en prod).

**Point de sécurité découvert en cours de route** : le rendu par
défaut de DataTables (`{ data: 'colonne' }`) insère la valeur en HTML,
pas en texte — vérifié empiriquement avec une charge `<img
onerror=...>` avant d'écrire le moindre template applicatif. Toutes
les colonnes texte simples utilisent `DataTable.render.text()` (natif,
échappe correctement — revérifié après coup). Les colonnes qui
construisent du HTML riche (montant + sous-ligne HT/TVA, badge de
statut, boutons d'action) passent chaque valeur dynamique par un
`escapeHtml()` maison (`datatables-fr.js`) avant interpolation.
Revérifié en navigateur réel avec la même charge XSS sur une dépense :
affichée en texte littéral, aucune exécution.

Vérifié manuellement (navigateur réel, sessions Direction et
Comptabilité) : pagination/tri/recherche sur Journal et Dépenses,
filtre "entité" du Journal câblé en AJAX, Valider/Refuser une dépense
de test avec un compte différent du saisissant (le jeton CSRF généré
depuis le JSON fonctionne bien en conditions réelles). Données de test
et entrées de journal associées nettoyées après coup.

**Backlog restant** (même pattern à réutiliser) :
- Dossiers enfants (Direction et Comptabilité), Personnel,
  utilisateurs Admin technique — volumétrie plus faible par nature
  d'après `documentation/ameliorations-performance.md`, priorité plus
  basse.

### Avances du fondateur (2026-08-07)

Même forme que Dépenses, migration directe du pattern déjà posé (pas
de nouvelle décision de conception) :
`MouvementFondateurRepository::findParEtablissementPagine()` (tri
whitelisté date/catégorie/montant/statut, recherche libre sur la
description), `MouvementFondateurService::historiquePagine()`
passthrough, route `GET /comptabilite/avances/donnees`, template en
table DataTables. Seule différence par rapport à Dépenses : les
boutons Valider/Refuser sont réservés à `ROLE_DIRECTION` (déjà le cas
avant, `is_granted('ROLE_DIRECTION')` dans le Twig) — devenu un
booléen `estDirection` passé une fois à la page (pas par ligne) et
vérifié côté JS avant d'injecter les boutons ; le contrôle serveur
reste inchangé (`denyAccessUnlessGranted('ROLE_DIRECTION')` sur les
routes `valider`/`refuser`), donc aucune perte de sécurité si le JS
était contourné.

Vérifié manuellement : le compte Direction voit les boutons
Valider/Refuser, le compte Comptabilité seul ne les voit pas ; clic
réel sur Valider (Direction, compte différent du saisissant) →
succès, statut et `validee_par` mis à jour en base. Donnée de test et
pièce jointe associée nettoyées après coup.

### Factures/Paiements — agrégation SQL du statut de paiement (2026-08-07)

Écran le plus à risque selon `documentation/ameliorations-performance.md`
(croissance illimitée, jamais purgé) et seul cas où le client a validé
explicitement l'option "calcul SQL dédié" plutôt que "pagination sans
filtre par statut pour l'instant" (l'autre option proposée), car le
besoin réel est de retrouver *toutes* les factures impayées, pas
seulement celles de la page affichée.

**Obstacle technique rencontré et résolu** : la première implémentation
tentait de sommer deux sous-requêtes DQL (`(SELECT ...) + (SELECT ...)`)
directement dans le `SELECT`/`WHERE` — DQL rejette ça (`ArithmeticPrimary`
n'accepte pas une sous-requête comme opérande arithmétique, seulement
comme comparaison isolée). Découvert en testant la requête en conditions
réelles (script contre la vraie base) avant même d'écrire le contrôleur.
Solution : `FactureRepository::findParEtablissementPagine()` construit la
requête de sélection/tri/pagination en **SQL natif** (`Connection::
createQueryBuilder()`), qui n'a pas cette limitation — récupère les ID de
factures (+ leur somme payée déjà agrégée) triés/filtrés/paginés, puis
une seconde requête DQL classique hydrate les entités `Facture`/`Enfant`
pour ces ID précis (réordonnées en PHP selon l'ordre SQL, qui n'est pas
garanti par `IN (...)`).

Nouveau DTO `App\Repository\LigneFacturePaiement` (facture + statut +
montant restant déjà calculés) : élimine au passage le N+1 documenté sur
cet écran (`PaiementService::statut()`/`montantRestant()` étaient
appelés une fois par facture affichée, chacun 2 requêtes). Le seuil de
comparaison (payé≤0/≥montant) reste dupliqué en PHP dans le repository —
à garder synchronisé avec `PaiementService::statut()` si la règle
change un jour (accepté comme trade-off par le client).

Vérifié : script direct contre la base comparant le résultat SQL natif
au calcul de référence (`PaiementRepository::sommePayee()` +
`CorrectionPaiementRepository::sommeValideePourFacture()`) sur 4
factures à statuts différents (dont une surpayée) — résultats
identiques. Puis vérification navigateur réelle (session Direction) :
pagination, tri par statut, recherche par numéro, clic sur le filtre
"Impayée" (rechargement AJAX, 2/4 factures) — tout correct, aucune
erreur console. Un bug a été trouvé et corrigé pendant cette
vérification : `Request::getEnum()` renvoie une 400 si le paramètre est
présent mais vide (le JS envoie toujours `statut=`, même sans filtre) —
corrigé en traitant la chaîne vide comme absence de filtre, comme pour
le filtre "entité" du Journal.

### Dossiers enfants (2026-08-07)

Dernier écran migré pour cette série. Mirror direct du pattern Journal
(lecture seule, pas d'actions/CSRF) sur les 2 écrans qui partagent
`EnfantRepository::findParEtablissementPagine()` — Direction
(`/administration/enfants`) et Comptabilité (`/comptabilite/enfants`),
chacun avec son propre lien de fiche (`administration_fiche_enfant` vs
`comptabilite_facture`) et son propre badge "Archivé" (avec date côté
Direction, sans côté Comptabilité — fidèle à l'existant). La recherche
libre DataTables fonctionne "gratuitement" ici (nom/prénom), sans
config supplémentaire, car `CriteresPagination::recherche` est déjà
câblé par défaut dans l'adaptateur.

**Décision de périmètre** (client) : on s'arrête là pour cette série.
Personnel et utilisateurs Admin technique restent non paginés —
`documentation/ameliorations-performance.md` les qualifie déjà de
"naturellement bornés" (effectif d'une crèche, poignée de comptes), et
Personnel en particulier aurait demandé de reconstruire plusieurs
actions CSRF par ligne (affecter à un groupe, révoquer/réactiver, accès
temporaire) côté JS pour un bénéfice faible. Les 4 écrans à risque de
croissance illimitée documentée (Dépenses, Journal, Avances,
Factures/Paiements) sont couverts.

Vérifié manuellement (sessions Direction et Comptabilité réelles) :
les deux écrans affichent les 3 enfants existants, liens de fiche
corrects par écran, badges "Archivé"/"Actif" fidèles à l'original.

## Prévention des doublons ResponsableLegal (2026-08-07)

`documentation/decisions-a-prendre.md` bloquait sur "fusion de
doublons" : `InscriptionService::admettre()` créait systématiquement un
nouveau `ResponsableLegal` à chaque admission, même pour une fratrie
déjà connue. Décision client : plutôt qu'un écran de fusion a
posteriori (portée ambiguë, gros chantier), **prévenir** le doublon à
la source. Règle de correspondance : même numéro de mobile OU même
email — le mobile est jugé fiable en Côte d'Ivoire (une ligne fixe
partagée y est rare), risque de faux-positif jugé négligeable. Si
correspondance trouvée : réutilisation **automatique** (pas de
confirmation Direction), avec mise à jour de la fiche existante par les
valeurs les plus récentes ("on garde toujours la valeur de la fiche
récente").

Deux points de création de `ResponsableLegal` avaient le même manque,
corrigés ensemble (même geste, cohérent plutôt qu'un correctif
partiel) :
- `InscriptionService::admettre()` — admission d'une pré-inscription.
- `FicheEnfantAdminService::creerEtAjouterResponsable()` — ajout manuel
  d'un second responsable par Direction sur une fiche existante.

Livré : `ResponsableLegalRepository::findParTelephoneOuEmail()`
(nouvelle méthode, seule à connaître le critère de correspondance,
scopée à l'établissement via une jointure sur `enfantsRattaches` — un
`ResponsableLegal` n'a pas de champ `etablissement` direct, utile pour
ne pas faire remonter un homonyme d'un autre établissement le jour du
multi-site). Les deux Services cherchent une correspondance avant de
créer ; si trouvée, réutilisation + mise à jour inconditionnelle des
champs plutôt qu'une nouvelle fiche.

Vérifié manuellement (script direct contre la base réelle, pas
seulement en tests unitaires) : deux admissions successives avec le
même téléphone responsable → une seule fiche `responsable_legal`
créée, les deux `EnfantResponsableLegal` pointent bien vers elle,
mise à jour confirmée avec les valeurs de la saisie la plus récente
(prénom et email de la 2ᵉ admission) ; `FicheEnfantAdminService::
creerEtAjouterResponsable()` réutilise bien la même fiche pour un
troisième enfant ; test de non-régression — un téléphone différent
crée bien une nouvelle fiche séparée. Données de test nettoyées après
coup.

## Suivi des notifications échouées (2026-08-07)

Dernier point ouvert de `documentation/decisions-a-prendre.md`, Module 3
f). `NotificationParentMailer`/`NotificationMessageMailer` catchent
`\Throwable` en interne et journalisent en `warning` (niveau log
applicatif, pas consultable à l'écran) sans jamais remonter l'échec à
l'appelant — aucune trace persistante, aucun écran de consultation.
Décision client : tracer uniquement les notifications jugées
importantes — sécurité/accès enfant (incident signalé, personne
autorisée activée/refusée), relance de facture impayée, messagerie
équipe↔parent. Le reste (photo/vidéo publiée, personne autorisée
proposée/ponctuelle entre parents) reste en best-effort silencieux,
comportement inchangé.

Livré :
- `NotificationParentMailer::notifierResponsables()` et
  `NotificationMessageMailer::notifierEquipe()`/`notifierFamille()`
  passent de `void` à `list<EchecNotification>` (nouveau value object,
  pas une entité) — le comportement best-effort/log `warning` interne
  est inchangé, seul le retour change. Les appels qui ignorent
  cette valeur de retour (photo, vidéo, personne autorisée
  proposée/ponctuelle) n'ont eu **aucune modification à faire** :
  ignorer un retour non-`void` est valide en PHP.
- Nouvelle entité `NotificationEchouee` (append-only, même moule que
  `JournalAudit`) + `NotificationEchoueeRepository`/`NotificationEchoueeService`
  (point d'écriture unique, même rôle que `JournalAuditService`).
  Pas de pagination sur l'écran de consultation
  (`/administration/notifications-echouees`, Direction) : volume
  attendu très faible, un échec d'envoi est l'exception, pas la norme
  — même raisonnement que Personnel/Admin technique laissés non
  paginés (chantier pagination du 2026-08-07 plus haut).
- `IncidentService::declarer()`, `PersonneAutoriseeService::validerPermanente()`/
  `refuserPermanente()`, `RelanceService::envoyer()`,
  `MessageService::notifier()` capturent désormais la liste des échecs
  et appellent `NotificationEchoueeService::enregistrer()` pour chacun.
  Cas particulier : la relance de facture rattache l'échec à la
  `Facture` (pas à la `Relance`, pas encore persistée — donc sans id —
  au moment de l'envoi) ; c'est de toute façon l'entité la plus utile
  pour Direction ("quelle facture n'a pas pu être relancée").

Vérifié manuellement (script direct contre la base réelle, avec un
`MailerInterface` construit à la main qui échoue systématiquement —
indépendant de `MAILER_DSN=null://null`, qui ne permet pas de
provoquer un vrai échec en dev) : `IncidentService::declarer()` réel
sur un enfant à deux responsables → deux lignes `notification_echouee`
créées (une par destinataire), avec le bon sujet/entité/id. Écran
`/administration/notifications-echouees` revérifié en navigateur, en
session Direction réelle. Données de test nettoyées après coup.

## Antivirus à l'upload — risque accepté (2026-08-07)

`documentation/decisions-a-prendre.md`/Module 7 (§10) notait ce point
comme "à ajouter avant tout déploiement en production réelle". Discuté
avec le client à l'aune de la cible d'hébergement réelle : mutualisé
OVH.

**Constat** : ClamAV classique (démon `clamd` + base de signatures via
`freshclam`) n'est pas installable sur un mutualisé — pas d'accès
root, pas de démon persistant possible, pas de gestionnaire de paquets
système. Même contrainte déjà actée pour écarter ffmpeg (vignettes
vidéo, cf. section "Sélecteur d'enfant + vidéos" plus haut). Les
alternatives (service de scan externe via API type Cloudmersive/
MetaDefender) impliqueraient d'envoyer les photos/documents des
enfants à un tiers pour analyse — écarté sans un fournisseur dont la
politique de rétention/confidentialité serait explicitement vérifiée
(exclut d'emblée les options gratuites type VirusTotal, qui partagent
les fichiers reçus avec la communauté sécurité).

**Décision client : risque accepté**, au vu des mitigations déjà en
place indépendamment d'un antivirus :
- Fichiers stockés hors `public/` (`var/photos/`, cf.
  `StockageLocalPhoto`) — jamais exécutables directement, jamais servis
  autrement que via un contrôleur de téléchargement à lien signé.
- Whitelist MIME + extension déjà appliquée à chaque upload
  (`finfo`, 8 Mo max, pdf/jpg/png uniquement — dupliquée dans
  `DocumentAdministratifService`, `MouvementFondateurService`,
  `PhotoService`, `DepenseService`, cf. Module 7 §10 dans
  `documentation/decisions-a-prendre.md`).

Ce que ce risque accepté laisse **non couvert** : un fichier "valide"
au sens MIME/extension (PDF ou image réellement conforme) mais
contenant un payload malveillant visant l'ordinateur de la personne
qui le télécharge ensuite (Direction, comptabilité) — pas un risque
d'exécution côté serveur.

→ À rouvrir si l'hébergement passe un jour sur un VPS OVH (root
disponible, ClamAV auto-hébergé redevient possible) ou si un besoin
réel de scan se confirme malgré la contrainte d'hébergement.

## "Mon profil" self-service — tous profils (remontée recette 2026-08-10)

**Fait le 2026-08-11.** Écran `/mon-profil`, accessible à tout compte
connecté quel que soit le rôle (`MonProfilController`,
`MonProfilService`) :
- civilité/nom/prénom modifiables par le titulaire du compte lui-même
  (email laissé hors périmètre — identifiant de connexion, pas demandé
  explicitement) ;
- changement de mot de passe avec confirmation par l'ancien mot de
  passe (`UserPasswordHasherInterface::isPasswordValid()`, même patron
  que `PersonneAutoriseeService::declarerAutorisationPonctuelleParent()`) ;
- avatar (JPEG/PNG, 3 Mo max) — réutilise `StockagePhotoInterface`
  (même principe que Photo/VideoEnfant, lien signé temporaire, jamais
  d'URL publique permanente), affiché dans le menu utilisateur du
  header (`AvatarExtension`, fonction Twig `avatar_url()`, même patron
  que `ParentsExtension`/`EquipeExtension`) à la place des initiales
  quand un avatar existe.

Vérifié en E2E manuel : modification informations, mot de passe changé
et re-connexion réussie avec le nouveau, avatar téléversé/affiché
partout/supprimé, lien signé refusé si signature invalide, écran
accessible Direction/Éducateur/Parent, redirection `/login` si non
connecté. Suite complète verte (452 tests).

## Calcul automatique du solde de congés (§19.2.b) — fait le 2026-08-12

Détail complet dans `documentation/suivi-caisses-rh.md` (section RH).
Résumé : `CongeService::calculerSolde()` estime le solde de congés
payés d'un salarié (mois travaillés depuis son premier contrat × taux
d'acquisition, moins les congés payés déjà pris), taux paramétrable
par établissement (`Etablissement::$joursCongesAcquisParMois`, défaut
2,2 jours/mois — droit ivoirien) depuis l'écran `/administration/rh`.
Les corrections manuelles (`AjustementSoldeConge`, append-only)
s'ajoutent au calcul automatique et ne sont jamais écrasées par un
recalcul.

## Seuil de rentabilité, tarifs par groupe et raison sociale — fait le 2026-08-24

Trois ajouts liés, à la demande de la Direction une fois les premiers
chiffres réels disponibles (loyer, tarifs TPS/PS/MS/GS et Crèche) :

- **`Etablissement::$raisonSociale`** — nom légal de l'entreprise
  ("Rive Digital"), distinct du nom commercial (`$nom`) déjà utilisé
  partout dans l'app (factures incluses). Champ interne, pas encore
  affiché sur les documents officiels — l'ajouter à la facture
  nécessiterait aussi RCCM/NIF/forme juridique (mentions OHADA), pas
  encore modélisés. Seule donnée de ce lot ajoutée par migration plutôt
  que par écran, faute d'écran de paramètres établissement existant —
  décision volontaire de ne **pas** faire de même pour les groupes
  (risque de collision avec des groupes déjà créés à la main en
  conditions réelles).
- **Tarif par groupe** — `Groupe::$fraisInscription`/`$tarifMensualite`/
  `$nombreMensualites`, paramétrable par la Direction sur l'écran
  Personnel (même écran que la création/édition de groupe). Sert de
  base au calcul de revenu par enfant.
- **`RentabiliteService::calculerSeuil()`** — compare les charges fixes
  mensuelles actives (`DepenseFixe::$montantHabituel`) au revenu mensuel
  moyen par enfant, calculé à partir du plan tarifaire de chaque
  groupe pondéré par son effectif réel (un groupe sans tarif renseigné
  ou sans enfant inscrit n'entre pas dans la moyenne). Affiché sur
  `/pilotage` avec un graphe (effectif actuel vs seuil estimé).
  Estimation indicative : ne modélise pas de coût variable par enfant
  (repas, couches...).

Jeux de données de recette alignés sur la réalité : 5 groupes réels
(Crèche/TPS/PS/MS/GS, remplacent "Bébés"/"Grands"), loyer réel
(1 200 000/mois, payé par trimestre), tarifs réels par groupe.

Vérifié en E2E manuel (upload/téléchargement, calcul du seuil avec
plusieurs groupes, changement de taux de congé sans écraser une
correction manuelle) + suite complète verte (471 tests avant l'ajout
suivant).

## Documents de l'établissement et plan de communication — fait le 2026-08-24

Deux besoins distincts remontés par la Direction :

- **Documents de l'établissement** (`/administration/documents`,
  réservé Direction) — statuts, bail, autorisation d'ouverture,
  assurance établissement, contrats fournisseurs..., classés par type
  (`DocumentEtablissement`/`TypeDocumentEtablissement`). Même
  mécanisme de stockage que les documents de fiche enfant
  (`DocumentAdministratif`) — clé opaque, jamais de chemin exposé,
  PDF/JPEG/PNG 8 Mo max — mais entité distincte : ces documents sont
  rattachés à l'établissement, pas à un enfant.
- **Plan de communication** (`/communication`) — argumentaire/script
  d'accueil en texte libre (modalités d'inscription, tarifs,
  horaires...), un seul par établissement (`PlanCommunication`).
  Lecture ouverte à tout le personnel opérationnel (Éducateur,
  Comptabilité, Cantine, Direction — nouvelle règle `access_control`
  multi-rôles, contrairement aux préfixes mono-rôle existants),
  édition réservée Direction (garde explicite dans le contrôleur, même
  patron que `PresenceController::declarerAutorisationPonctuelle()`).
  Décision volontaire : lecture large plutôt que Direction-only, pour
  que quiconque décroche le téléphone puisse s'y référer.

Vérifié en E2E manuel : upload/téléchargement de document, 403 pour un
compte Éducateur sur `/administration/documents`, lecture du plan de
communication par un compte Éducateur, écriture refusée (403) en POST
direct par ce même compte, écriture acceptée par Direction. Suite
complète verte (471 tests).
