docs(notification) : spec et plan d'implémentation des notifications de tâche

This commit is contained in:
Matthieu
2026-06-15 11:45:22 +02:00
parent 9e63f3d268
commit 1351bbf1b1
2 changed files with 564 additions and 0 deletions
@@ -0,0 +1,126 @@
# 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.