# 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.
