6.4 KiB
Notifications sur événements de tâche — Design
Date : 2026-06-15 Ticket lié : (à créer) — recâblage du système de notifications
Contexte & problème
Le système de notifications de Lesstime est aujourd'hui une coquille vide : toute la
plomberie consommatrice existe encore (entité Notification, NotificationProvider,
NotificationRepository, NotificationUnreadCountController, MarkAllReadController,
et côté front NotificationBell.vue + useNotifications.ts + services/notifications.ts
qui poll toutes les 2 min), mais plus aucun producteur ne crée de notification.
Cause : le seul producteur était NotificationService, déclenché par les ClientTicket
du portail client. Le commit 2a0b202 (« suppression du portail client ») a retiré
ClientTicket, NotificationService et les processors associés, laissant la cloche
interroger /notifications/unread-count dans le vide. Le compteur reste donc à 0 et le
dropdown est toujours vide.
Le travail récent LST-52 (pagination du
NotificationProvider) est correct mais portait sur une liste structurellement toujours vide.
Objectif
Rebrancher la création de notifications sur des événements réels qui existent encore dans l'app : les événements de tâche.
Périmètre (MVP)
Déclencheurs & destinataires
| Événement | Détection (changeset Doctrine) | Destinataire | Type |
|---|---|---|---|
Tâche assignée (création ou modif où assignee passe à un nouvel user) |
assignee : old ≠ new et new ≠ null |
le nouvel assigné | task_assigned |
| Collaborateur ajouté | insertDiff sur la collection collaborators |
chaque user ajouté | task_collaborator_added |
Règles :
- Auto-exclusion : si le destinataire == l'acteur courant, aucune notification.
- Réassignation A→B : seul B est notifié (pas de notification « désassigné » — hors scope).
assigneepasse ànull: aucune notification.- Si plusieurs personnes deviennent destinataires dans un même flush, chacune reçoit sa notification.
Contenu des notifications
Réutilise l'entité Notification existante (user, type, title, message,
isRead, createdAt) — aucune migration.
task_assigned→ titre « Nouvelle tâche assignée », message«{titre tâche}» — {nom projet}.task_collaborator_added→ titre « Ajout à une tâche », message«{titre tâche}» — {nom projet}.
Décisions de comportement
- Pas d'acteur authentifié → pas de notification. Les deux chemins utilisateurs réels
(frontend JWT, MCP token) ont toujours un user authentifié. CLI / fixtures / cron de
récurrence n'ont pas d'acteur → aucune notification. Effet de bord positif :
make fixturesne génère pas de notifications parasites. - Pas de lien cliquable vers la tâche dans cette itération (l'entité
Notificationn'a pas de champ URL ; la cloche affiche titre + message + date relative). Extension future possible, hors scope MVP.
Architecture
Approche retenue : listener Doctrine onFlush / postFlush (un seul point de vérité
qui couvre tous les chemins d'écriture — frontend API Platform, MCP, et tout futur chemin —
puisque tous persistent via EntityManager::flush()).
Approches écartées :
- Décorateur de processor API Platform + hooks dans les tools MCP : logique dupliquée sur plusieurs endroits, risque d'oublier un chemin (c'est exactement ce type d'oubli qui a créé le bug initial).
- Événements de domaine + Symfony Messenger async : surdimensionné pour 2 événements, ajoute transport + worker (YAGNI).
Composant : App\EventListener\TaskNotificationListener
Enregistré via #[AsDoctrineListener] sur les événements onFlush et postFlush.
Dépendances injectées : Symfony\Bundle\SecurityBundle\Security (acteur courant).
onFlush(OnFlushEventArgs $args) — collecte (ne persiste rien encore) :
$uow = $em->getUnitOfWork();- Acteur :
$actor = $this->security->getUser();→ sinull, on sort (aucune notif). - Assignations :
getScheduledEntityInsertions(): pour chaqueTaskinsérée avecassignee !== nulletassignee !== actor→ file(assignee, 'task_assigned', task).getScheduledEntityUpdates(): pour chaqueTask,getEntityChangeSet($task); siisset($cs['assignee'])avec[$old, $new] = $cs['assignee'],$new !== nullet$new !== actor→ file(new, 'task_assigned', task).
- Collaborateurs :
getScheduledCollectionUpdates(): pour chaquePersistentCollectiondont l'owner est uneTasket le champ vautcollaborators,getInsertDiff()donne les users ajoutés ; pour chacun!== actor→ file(user, 'task_collaborator_added', task).
- Stocke la file dans une propriété privée du listener.
postFlush(PostFlushEventArgs $args) — persiste :
- Si la file est vide, retour immédiat.
- Vide la file dans une variable locale puis réinitialise la propriété (anti-réentrance).
- Pour chaque entrée, crée une
Notification(user, type, title, message, createdAt),persist. $em->flush()une seconde fois. Pas de boucle infinie : lesNotificationne sont pas desTask, donc ce second flush ne reschedule aucune assignation/collaboration.
Tests (PHPUnit, make test)
Cas couverts :
- Assignation d'une tâche à un user (par un autre acteur) → 1 notification
task_assignedpour cet user. - Auto-assignation (acteur s'assigne la tâche) → aucune notification.
- Ajout d'un collaborateur → 1 notification
task_collaborator_addedpour cet user. - Réassignation A→B → seul B reçoit une notification.
assigneepassé ànull→ aucune notification.- Pas d'acteur authentifié (contexte CLI) → aucune notification.
Hors périmètre
- Notifications de changement de statut, d'échéance proche, de désassignation.
- Lien cliquable / navigation vers la tâche depuis la notification.
- Préférences utilisateur (opt-in/opt-out par type), notifications e-mail.
- Modification du front (la cloche consomme déjà l'API et s'affichera dès que des notifications existent).
Fichiers impactés
- Nouveau :
src/EventListener/TaskNotificationListener.php - Nouveau : tests PHPUnit (
tests/EventListener/ou emplacement équivalent au projet). - Aucune migration, aucun changement d'entité, aucun changement front.