Files
SIRH/docs/superpowers/specs/2026-06-24-contract-end-notification-design.md
T
2026-06-24 15:08:38 +02:00

7.1 KiB

Notification de fin de contrat (veille du dernier jour) — Design

Date : 2026-06-24 Branche : feature/SIRH-43-ajouter-une-notif-la-veille-d-un-contrat-qui-se-te Statut : Validé (brainstorming)

Objectif

Prévenir automatiquement les administrateurs, sur le dernier jour ouvré précédant la fin d'un contrat, qu'un salarié arrive au terme de son emploi — afin qu'ils puissent anticiper (solde de tout compte, désactivation des accès, etc.).

La notification réutilise le système de notification existant (entité Notification, cloche admin dans AppTopNav.vue). Aucune migration de base de données.

Décisions de cadrage

Sujet Décision
Déclencheur Vraie fin d'emploi uniquement : la période qui se termine est la dernière période de contrat du salarié (aucune période ne lui succède). Un changement de contrat enchaîné (ex. CDD 35h → CDI 39h) ne notifie pas.
Timing Le dernier jour ouvré strictement avant endDate, en sautant week-ends ET jours fériés.
Message « Fin de {nature} de {Prénom Nom} le {dd/mm/yyyy} » — nature = libellé FR (CDI / CDD / Intérim).
Catégorie Contrat
Cible du clic /employees/{id} (fiche employé).
Destinataires Tous les ROLE_ADMIN (UserRepository::findAllAdmins()).
Acteur null (notif générée par un job automatique, pas par un utilisateur).
Déclenchement Commande console quotidienne via crontab prod (~6h du matin).

Rappels sur l'existant

  • Notification (table notifications) : recipient (NOT NULL), actor (nullable), message, category, target, isRead, createdAt. Exposé via getActorName().
  • endDate d'une EmployeeContractPeriod est inclusif : c'est le dernier jour couvert par le contrat (findOneCoveringDate : endDate >= :date).
  • Pattern de notif existant : WorkHourSiteValidationProcessor crée une Notification par admin (findAllAdmins) avec new Notification() + persist + flush. Pas de service factory.
  • Pattern cron existant : RttRolloverCommand / LeaveRolloverCommand (#[AsCommand], logger monolog.logger.cron, options --force/--recompute). Déclenchées par le crontab système (pas de Symfony Scheduler dans le projet).
  • PublicHolidayService : source des fériés (cache 30j), déjà en place.

Approche retenue

Commande cron quotidienne + service métier dédié et testable.

Alternatives écartées :

  • Symfony Scheduler (Messenger) : brique non utilisée dans le projet, inutile ici.
  • Calcul à la volée dans le provider : casse isRead/historique, recalcul à chaque ouverture de la cloche, mélange notifs persistées et virtuelles.

Conception détaillée

1. Détection (cœur métier)

Nouveau service App\Service\Notification\ContractEndNotificationService.

Algorithme (date du jour T injectable pour les tests) :

  1. Si T est un week-end ou un férié → sortie (aucun jour chômé ne génère de notif).
  2. Calculer N = prochain jour ouvré strictement après T (saute week-ends + fériés via PublicHolidayService).
  3. Charger en une seule requête la dernière période de chaque employé (EmployeeContractPeriodRepository::findLatestPeriodsForAllEmployees()).
  4. Candidat = dernière période dont endDate est non nul et vérifie T < endDate <= N.
    • Le test « dernière période » assure nativement la règle « vraie fin d'emploi » : un changement de contrat enchaîné a une période suivante, donc n'est jamais la dernière.
    • endDate = null (CDI ouvert) → jamais candidat.

Exemples :

  • Mardi (T), N = mercredi → notifie les contrats finissant mercredi (J-1 classique).
  • Vendredi (T), N = lundi → notifie les contrats finissant samedi, dimanche ou lundi.
  • Vendredi (T), lundi férié → N = mardi → notifie samedi…mardi.
  • Week-end (T) → rien.

2. Création des notifications & idempotence

Pour chaque candidat :

  • Message : Fin de {nature} de {Prénom Nom} le {endDate->format('d/m/Y')}.
  • Une Notification par admin : recipient=admin, actor=null, category='Contrat', target='/employees/{id}'. Persist groupé, un seul flush() final.

Idempotence (le job peut être relancé le même jour) : avant création, vérifier qu'il n'existe pas déjà une notif identique pour ce destinataire via (recipient, category='Contrat', target='/employees/{id}', message) exact. Le message étant unique par (employé + date + nature), cela empêche tout doublon — y compris après que l'admin a lu la notif (isRead=true). Nouvelle méthode NotificationRepository::existsForRecipientTargetMessage(...) (ou findOneBy). Pas de migration : on ne stocke pas de FK « période » sur Notification.

3. Affichage front (cloche)

AppTopNav.vue rend aujourd'hui **{actorName}** {message}. Pour actorName vide :

  • N'afficher que {message} (pas de span gras vide ni capitalize orphelin).
  • Le reste est inchangé : avatar/pastille, formatTimeAgo + catégorie « Contrat », point non-lu, lien target.

Aucune route ni service front nouveau : category et target passent par le DTO existant. Cloche déjà admin-only → rien d'autre côté visibilité.

4. Commande console

App\Command\ContractEndNotificationCommandapp:contract:end-notifications.

  • Délègue tout au service.
  • Option --date=YYYY-MM-DD : forcer la date du jour (tests / rattrapage manuel).
  • Logger monolog.logger.cron. Sortie SymfonyStyle (nb de notifs créées / employés concernés).
  • Idempotente par construction (cf. §2) → relançable sans risque.
  • Crontab prod (infra) : 0 6 * * * (tous les jours, 6h). Pas de restriction jour de semaine (la commande s'auto-neutralise week-ends/fériés).

Fichiers

Backend — nouveaux

  • src/Service/Notification/ContractEndNotificationService.php
  • src/Command/ContractEndNotificationCommand.php
  • tests/... (tests du service)

Backend — modifiés

  • src/Repository/EmployeeContractPeriodRepository.phpfindLatestPeriodsForAllEmployees()
  • src/Repository/NotificationRepository.php — existence anti-doublon
  • Helper « jour ouvré » (week-end + férié) — dans le service ou petit util réutilisable

Frontend — modifié

  • frontend/components/AppTopNav.vue — gérer actorName vide

Docs

  • doc/functional-rules.md — compléter la section 15) Notifications
  • doc/contract-end-notifications.md — nouveau (règle complète)
  • frontend/data/documentation-content.ts — entrée admin
  • CLAUDE.md — note du nouveau pattern (commande cron de notification)

Pas de migration DB.

Tests (PHPUnit)

Cœur isolé dans ContractEndNotificationService (date T fixe + PublicHolidayService mocké) :

  • Fin mercredi, T=mardi → 1 notif/admin.
  • Fin lundi, T=vendredi → notifié vendredi ; rien samedi/dimanche.
  • Lundi férié + fin mardi, T=vendredi → notifié vendredi (N saute le lundi férié).
  • Période suivante existante (changement enchaîné) → pas de notif.
  • endDate=null → pas de notif.
  • Idempotence : 2ᵉ exécution même jour → aucun doublon.
  • T = week-end → aucune création.