# 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). - `assignee` passe à `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 1. **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 fixtures` ne génère pas de notifications parasites. 2. **Pas de lien cliquable** vers la tâche dans cette itération (l'entité `Notification` n'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) : 1. `$uow = $em->getUnitOfWork();` 2. Acteur : `$actor = $this->security->getUser();` → si `null`, **on sort** (aucune notif). 3. Assignations : - `getScheduledEntityInsertions()` : pour chaque `Task` insérée avec `assignee !== null` et `assignee !== actor` → file `(assignee, 'task_assigned', task)`. - `getScheduledEntityUpdates()` : pour chaque `Task`, `getEntityChangeSet($task)` ; si `isset($cs['assignee'])` avec `[$old, $new] = $cs['assignee']`, `$new !== null` et `$new !== actor` → file `(new, 'task_assigned', task)`. 4. Collaborateurs : - `getScheduledCollectionUpdates()` : pour chaque `PersistentCollection` dont l'owner est une `Task` et le champ vaut `collaborators`, `getInsertDiff()` donne les users ajoutés ; pour chacun `!== actor` → file `(user, 'task_collaborator_added', task)`. 5. Stocke la file dans une propriété privée du listener. **`postFlush(PostFlushEventArgs $args)`** — persiste : 1. Si la file est vide, retour immédiat. 2. Vide la file dans une variable locale puis **réinitialise la propriété** (anti-réentrance). 3. Pour chaque entrée, crée une `Notification` (user, type, title, message, createdAt), `persist`. 4. `$em->flush()` une seconde fois. Pas de boucle infinie : les `Notification` ne sont pas des `Task`, 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_assigned` pour cet user. - Auto-assignation (acteur s'assigne la tâche) → **aucune** notification. - Ajout d'un collaborateur → 1 notification `task_collaborator_added` pour cet user. - Réassignation A→B → seul **B** reçoit une notification. - `assignee` passé à `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.