---
suivi: 1544
date: 2026-09-15
sujet: Diagnostic — mention commentaire tâche, alerte et Teams
chantier: taches_suivi / mentions / alertes / teams
type: diagnostic
statut: poussé
hash: b75661e1
fichiers:
  - docs/suivi/SUIVI_1544_diag_mention_tache_notification.md
---

# SUIVI #1544 — PHASE 1 diagnostic : notifier une mention dans une tâche de suivi

## PROMPT ENVOYÉ

Diagnostic seul. Une mention dans un commentaire de tâche n’envoie
apparemment ni Teams ni alerte Hectarion, alors que la mention facture
crée bien une alerte « vous a mentionné sur une facture ». Retracer
les deux circuits, le canal Teams, les traces d’échec, le uuid du
markup, le scénario d’alerte, qui notifier, le volume SQL. Aucune
migration, aucun code applicatif. Fiche seule.

## BASE GIT

HEAD local = `88777d08` (#1543, ahead of origin).
`origin/main` au départ du diagnostic = `a9bf911b` (#1542 journal).
Aucun fichier applicatif modifié par ce lot.

## FICHES LUES

- **#1237 LEÇON** — auto-mention enregistrée ; skip notif **par
  destinataire** (auteur). `TacheSuiviService.php` ~L623. Ne pas
  confondre enregistrement et notification.
- **#1077 LEÇON** — HTTPS sortant mort en CLI OVH. Teams/Graph depuis
  middleware web `terminate` (digest) ou requête PHP-FPM. Jamais cron
  SSH. Impact : PHASE 2 ne doit pas dispatcher un job / `php artisan`.
- **#1516 LEÇON** — visibilité **facture** : égalité SQL sur id
  numérique ou e-mail, pas azure_id. **Hors sujet mention** : le
  markup `@[Nom](uuid)` est l’`azure_id` Graph, résolu via
  `users.azure_id` (`ensureUserFromGraph`). Ne pas appliquer le
  diagnostic dépôt au lookup mention.
- **#1542** — page `/alertes` : module `taches_suivi` → étiquette
  « Tâches » ; `type=info` = étiquette grise, pas ambre. Le scénario
  mention tâche **existe déjà** ; pas de retouche d’affichage.
- **#1211 / #1077** — digest 9h : `DeclencheDigestQuotidien` +
  `Artisan::call('taches:digest')` dans `terminate` PHP-FPM.

Aucune fiche n’a changé l’approche : le trou n’est pas « parsing
seul », c’est « cloche conditionnelle + Teams jamais branché ».

---

## 1. LE MÉCANISME QUI FONCTIONNE — FACTURES (patron)

Chaîne **synchrone dans la requête web** POST commentaire.

| Étape | Fichier:lignes | Rôle |
|-------|----------------|------|
| Saisie `@` | `resources/js/Components/Facture/FactureDiscussionFil.vue` (import `factureCommentMentions.js` L9, payload L80) | Brouillon `@Nom` → body `@[Nom](azure_id)` + tableau `mentions` |
| Markup | `resources/js/utils/factureCommentMentions.js` L135-163 | `buildBodyWithMentionTokens` / `buildMentionsPayloadFromDraft` |
| Recherche | `GraphUserSearchController::search` L15-36 + `facture_id` | Graph `id` = object id Azure |
| POST | `FactureCommentaireController` (~L28-40) | `mentions.*.azure_id` required_with |
| Parse | `FactureCommentaireMentionParser::normaliser` L14-59 | Payload **et** regex `@\[([^\]]+)\]\(([^)]+)\)` sur le body |
| Résolution user | `TaskSyncService::ensureUserFromGraph` L27-71 | Lookup `users.azure_id` puis `users.email` |
| Pivot | `facture_commentaire_mentions` (migration `2026_06_26_190000` L14-25) | `user_id` **obligatoire**, `azure_id` varchar(64) |
| Skip soi | `FactureCommentaireService` L126-129 | Mention stockée ; pas d’alerte à l’auteur (#1237) |
| **Cloche** | `AlerteService::notifierMentionFactureCommentaire` L331-379 | `updateOrCreate` table `alertes` |
| « Teams » | `MentionNotificationDispatcher::dispatch` L17-23 | **no-op** (log info seulement) |

Alerte créée :

- `module` = `factures` (L372)
- `type` = `Alerte::TYPE_INFO` (L373)
- `scenario` = `facture_commentaire_mention_dest_{id}` (constante L75 + suffixe L361)
- `user_id` = le mentionné (L370)
- `entity_type` = `App\Models\FactureCommentaire`, `entity_id` = id commentaire
- Titre : `{auteur} vous a mentionné sur une facture` (L374) — celui vu en prod

**Pas de test `estVisiblePar`** : dès que le user ERP est résolu et n’est
pas l’auteur, l’alerte est créée. C’est pour ça que la cloche facture
« marche ».

**Teams facture mention : ne marche pas non plus.** Le dispatcher L10-14
dit explicitement d’attendre Teams Activity Feed (#446). Aucun
`Http::post` n’est déclenché. Si l’utilisateur croit qu’un Teams part
pour une mention facture, ce n’est pas ce code. Les Teams qui marchent
sont d’autres circuits (digest, assignation tâche, clôture, offres
foncières / juridique via `EntiteCommentaireService`).

---

## 2. LE MÉCANISME DES TÂCHES

Même parser, même Graph, même `ensureUserFromGraph`, même
`AlerteService` jumeau. **Ce n’est pas « parsé et stocké sans code de
notification ».** La notification cloche **existe** ; Teams mention **n’est
pas branché** (même no-op que les factures).

| Étape | Fichier:lignes | Rôle |
|-------|----------------|------|
| Placeholder | `TacheSuiviCommentaireInput.vue` L169 | « (@ pour mentionner) » |
| `azureId` | même fichier L118 | `String(user.id)` = Graph object id |
| POST | `TacheSuiviDetailModal.vue` L367 ; `Pages/TachesSuivi/Show.vue` L269 | `{ body, mentions }` tel quel |
| Validation | `StoreTacheSuiviCommentaireRequest` L20-24 | `mentions.*.azure_id` **nullable** (facture : required_with) |
| Contrôleur | `TacheSuiviController::storeCommentaire` L187-205 | passe `mentions` à `ajouterCommentaire` |
| Create | `TacheSuiviService::ajouterCommentaire` L347-367 | **dans** `DB::transaction` |
| Traite | `traiterMentions` L583-647 | parse → Graph user → pivot → éventuellement alerte |

Pivot `tache_suivi_commentaire_mentions` (migration
`2026_07_02_100500` L75-81) : `user_id` **nullable**, `azure_id`
nullable. Table prévue par le dictionnaire.

### 2.1 La cloche EST prévue

`AlerteService::notifierMentionTacheSuiviCommentaire` L386-431 :

- `module` = `taches_suivi` (L424)
- `type` = `TYPE_INFO` (L425)
- `scenario` = `tache_suivi_commentaire_mention_dest_{id}` (constante L77 + L413)
- titre : `{auteur} vous a mentionné sur une tâche de suivi` (L426)
- `lien` = `route('taches-suivi.show', $tache)` (L406)
- skip auteur L392-394 (doublon du skip service)

Appel : `TacheSuiviService` L637-646, puis
`MentionNotificationDispatcher::dispatch` — **no-op Teams**.

Journal utilisateur `taches-suivi-documents-mentions-2026-07-01`
(`journal_mises_a_jour_post_2026_06_12.php` L1177-1181) : « il reçoit
une notification cloche […] **(s’il y a accès)** ».

### 2.2 Pourquoi ça peut ne rien produire (cloche)

Trois coupures **avant** l’alerte, toutes dans `traiterMentions` :

1. **Graph non résolu** L606-613 — `ensureUserFromGraph` retourne null
   (azure_id inconnu **et** e-mail vide, L54-55 de `TaskSyncService`).
   `Log::warning('Mention tâche suivi : utilisateur Graph non résolu')`.
   **Pas de ligne pivot** (continue avant le `create` L616). Cas
   « markup dans le body, zéro ligne `tache_suivi_commentaire_mentions` ».

2. **Auto-mention** L623-626 — pivot **oui**, alerte **non** (#1237).

3. **`estVisiblePar($mentionne)` faux** L628-634 —
   `Log::info('Mention tâche suivi : notification omise (utilisateur sans visibilité')`.
   Pivot **oui**, alerte **non**. C’est **la différence structurelle
   avec les factures**. `estVisiblePar` L253-258 relit
   `scopeVisiblePourUtilisateur` L317+ : créateur, `tache_suivi_personnes`,
   étiquette programme, projet, ou ACL dossier si morph dossier.
   Mentionner un collègue qui n’est ni associé ni créateur ni dans le
   périmètre → mention visible dans le fil, **zéro alerte**. Produit
   documenté (« s’il y a accès »), pas un crash.

Verdict diagnostic :

| Question | Réponse |
|----------|---------|
| Mentions seulement parsées, code notif absent ? | **Non.** Cloche codée (#531 / L637). |
| Notification existe mais Teams échoue ? | Teams mention **n’est jamais tenté** (dispatcher no-op L17-23). |
| Cloche peut être omise sans erreur 500 ? | **Oui**, volontairement si pas d’accès (L628) ou soi (L623) ou Graph null (L606). |

Pour départager en prod : SQL §8 (pivot sans alerte vs pivot+alerte)
+ grep logs §4.

---

## 3. POURQUOI TEAMS N’ARRIVE PAS — POINT CRITIQUE

### 3.1 Canal technique des envois qui marchent

`TeamsNotificationService` (`app/Services/TeamsNotificationService.php`
L12-56) : `Http::timeout(10)->post($webhookUrl, …)` avec
`cible_type=utilisateur` + `destinataire_email`. URL =
`config('services.teams.webhook_url')` = `TEAMS_WEBHOOK_URL`
(`config/services.php` L144-145). C’est un **webhook Power Automate /
Logic Apps**, pas Graph `TeamsActivity.Send`. Host typique
`logic.azure.com` / `powerplatform.com` / `webhook.office.com`
(`LogOutboundHttpCall` L177-181).

### 3.2 Mention tâche / facture

`MentionNotificationDispatcher` L17-23 : **aucun HTTP**. Donc :

- pas d’échec webhook ;
- pas de ligne `api_logs` `service=teams` imputable à une mention ;
- un cron n’est **pas** en cause ici (rien n’est mis en file).

### 3.3 Synchrone vs job vs cron

| Circuit | Quand | Contexte | Teams réel ? |
|---------|-------|----------|----------------|
| Mention facture / tâche | Dans le POST web, après `updateOrCreate` alerte | PHP-FPM | **Non** (no-op) |
| Mention offre / juridique / registre | `EntiteCommentaireService` L105-129, même requête | PHP-FPM | **Oui** `notifierUtilisateur` |
| Assignation personnes tâche | `TacheSuiviService::notifierAssignationPersonnes` L903-930 | PHP-FPM (transaction create/attach) | **Oui** |
| Clôture tâche | `notifierClotureTache` L938-992 | PHP-FPM | **Oui** |
| Digest 9h | `DeclencheDigestQuotidien::terminate` L64-75 → `Artisan::call('taches:digest')` | **PHP-FPM terminate**, pas SSH | **Oui** |
| Alertes foncières | `DeclencheFoncierAlertes` terminate → `FoncierAlerteEvaluationService` L157 | PHP-FPM terminate | **Oui** |

`QUEUE_CONNECTION=sync` ne sauve pas un job lancé depuis CLI SSH
(#1077 L94-96). Le digest **n’est pas** un cron OVH : le premier GET
authentifié après 9h Paris (jours ouvrables, L36-42) pose un verrou
`taches_digest_runs` (L45-50) puis envoie en `terminate`.

### 3.4 Contournement OVH (ce qui marche aujourd’hui)

Tout `Http::` Teams part d’une requête web FastCGI. PHASE 2 doit
copier `EntiteCommentaireService::notifierTeamsMention` (L111-129) :
appel **direct** à `TeamsNotificationService` dans le POST commentaire
tâche, **après** création d’alerte, **sans** `ShouldQueue`, **sans**
`php artisan`. Ne pas réactiver le dispatcher no-op pour les factures
(garde-fou du prompt).

---

## 4. TRACES D’ÉCHEC

| Source | Ce qu’on y voit pour une mention tâche |
|--------|----------------------------------------|
| `Log::warning` L607 | Graph non résolu (`azure_id` dans le contexte) |
| `Log::info` L629 | Notif omise (pas d’accès) |
| `Log::info` dispatcher L19 | Hook Teams no-op **si** alerte créée |
| `Log::error` `[Teams]` | **Pas** pour les mentions tâche/facture (pas d’appel). Utile pour digest / assignation / clôture |
| `api_logs` | Uniquement si `Http::` part (`LogOutboundHttpCall` L17-20). Mentions tâche = **zéro ligne** attendue |
| Table dédiée d’échec mention | **Aucune** |

Commandes / SQL de vérif (à coller en prod) : voir synthèse § traces.
Fichier log : `storage/logs/laravel.log` (rotation `laravel-YYYY-MM-DD.log`
possible).

---

## 5. LE FORMAT DE MENTION

Markup stocké dans `tache_suivi_commentaires.body` :
`@[Nom](uuid)` — parser PHP L35, JS L13 / L50.

Le `uuid` est **`users.azure_id`** = object id Microsoft Graph
(`GraphApiService::mapTenantUsersForSelect` L345 : `'id' => $user['id']`
Graph ; front L118 `azureId: String(user.id)`).

Ce n’est **pas** `users.id` (entier auto-inc).

Lookup : `ensureUserFromGraph` L36-44 `where('azure_id', $azureId)`
puis `orWhere('email')`. Si l’utilisateur ERP existe déjà (cas Laury /
Robin), l’azure_id suffit, e-mail optionnel.

**Rapport #1516** : le scope visibilité **dépôt facture** compare un id
numérique ou un e-mail, jamais azure_id. Ici la clé utilisable pour
retrouver le destinataire d’une mention est **`users.azure_id`**, puis
`users.id` une fois le modèle chargé (alerte.user_id, pivot.user_id).
Ne pas résoudre le markup avec `users.id`.

---

## 6. L’ALERTE HECTARION

Table `alertes` (`2026_05_11_160000` L11-27) : `scenario` VARCHAR(64),
unicité `(entity_type, entity_id, scenario)`.

Constante déjà là : `SCENARIO_TACHE_SUIVI_COMMENTAIRE_MENTION`
= `tache_suivi_commentaire_mention` (33 car.) + `_dest_` + id user
≪ 64. **Pas de nouveau scénario à inventer. Pas de migration.**

#1542 (`Alerte::MODULE_LIBELLES` L23-25) : `taches_suivi` → « Tâches ».
`TYPE_INFO` → étiquette grise, pas ambre. Titre/message déjà rendus
avec mentions (`AlerteTexteAvecMentions`). **Aucune retouche page
alertes / cloche** pour brancher ce scénario : s’il est inséré, il
apparaît.

Cloche nav : `GET /alertes/count` (`AlerteController::count`) —
`unread()` sur `user_id` / groupe. Alerte mention = `user_id` du
mentionné, `groupe_cible` null → compteur OK sans changer AppShell.

---

## 7. QUI EST NOTIFIÉ — OPTIONS SANS TRANCHER

**Aujourd’hui (code)** : uniquement la personne du token `@`, si
résolue, si ≠ auteur, **et** si `estVisiblePar`. Ni les autres
`tache_suivi_personnes`, ni le créateur, ni `demande_suiveurs`
(`demande_suiveurs` = suiveurs **demande juridique**, pas les tâches).

**Auto-mention** : pivot oui, cloche/Teams non (#1237 L623-626 et
AlerteService L392). À conserver.

Options PHASE 2 (produit) :

| Id | Destinataire cloche+Teams | Effet de bord |
|----|---------------------------|---------------|
| A | Mentionné **avec** accès (statut quo cloche) + **ajouter Teams** | Aligné journal L1181 ; le constat prod peut rester si on mentionne hors périmètre |
| B | Mentionné **même sans** accès (comme factures) + Teams | Cloche+Teams pour tout collègue tagué ; le lien peut 403 s’il n’a vraiment pas le droit |
| C | B + auto-attacher sur `tache_suivi_personnes` | Donne l’accès puis notifie ; change le métier « personnes associées » |
| D | Mentionné (A ou B) **et** copie aux associés / créateur | Spam ; hors besoin strict « la personne mentionnée » |

Recommandation technique **sans décision métier** : A si le SQL §8
montre des alertes pour les gens déjà sur la tâche (alors seul Teams
manque) ; B si le SQL montre beaucoup de pivots **sans** alerte pour
des user_id qui existent (alors le filtre L628 explique le constat).

---

## 8. VOLUME — REQUÊTES SQL (à exécuter en prod)

Morph Laravel : `entity_type` = `App\\Models\\TacheSuiviCommentaire`.

```sql
-- 8.1 Mentions tâche : volume et période
SELECT
  COUNT(*) AS nb_mentions,
  COUNT(user_id) AS nb_avec_user_erp,
  MIN(created_at) AS premiere,
  MAX(created_at) AS derniere
FROM tache_suivi_commentaire_mentions;

-- 8.2 Mentions vs alertes cloche (même commentaire + même destinataire)
SELECT
  COUNT(*) AS nb_mentions,
  SUM(a.id IS NOT NULL) AS nb_avec_alerte,
  SUM(a.id IS NULL) AS nb_sans_alerte
FROM tache_suivi_commentaire_mentions m
INNER JOIN tache_suivi_commentaires c
  ON c.id = m.tache_suivi_commentaire_id
LEFT JOIN alertes a
  ON a.entity_type = 'App\\Models\\TacheSuiviCommentaire'
 AND a.entity_id = c.id
 AND a.user_id = m.user_id
 AND a.scenario = CONCAT('tache_suivi_commentaire_mention_dest_', m.user_id);

-- 8.3 Commentaires avec markup @[...](...) mais sans ligne pivot
SELECT COUNT(*) AS commentaires_markup_sans_pivot
FROM tache_suivi_commentaires c
WHERE c.deleted_at IS NULL
  AND c.est_systeme = 0
  AND c.body LIKE '%@[%](%'
  AND NOT EXISTS (
    SELECT 1 FROM tache_suivi_commentaire_mentions m
    WHERE m.tache_suivi_commentaire_id = c.id
  );

-- 8.4 Dernières mentions (contrôle manuel)
SELECT m.id, m.created_at, m.user_id, m.azure_id, m.display_name,
       c.id AS commentaire_id, c.tache_suivi_id,
       a.id AS alerte_id, a.lu_at, a.scenario
FROM tache_suivi_commentaire_mentions m
INNER JOIN tache_suivi_commentaires c ON c.id = m.tache_suivi_commentaire_id
LEFT JOIN alertes a
  ON a.entity_type = 'App\\Models\\TacheSuiviCommentaire'
 AND a.entity_id = c.id
 AND a.user_id = m.user_id
ORDER BY m.id DESC
LIMIT 30;

-- 8.5 Référence facture (le circuit qui « marche » en cloche)
SELECT
  COUNT(*) AS nb_mentions_facture,
  SUM(a.id IS NOT NULL) AS nb_avec_alerte
FROM facture_commentaire_mentions m
INNER JOIN facture_commentaires c ON c.id = m.facture_commentaire_id
LEFT JOIN alertes a
  ON a.entity_type = 'App\\Models\\FactureCommentaire'
 AND a.entity_id = c.id
 AND a.user_id = m.user_id;
```

Si 8.3 > 0 : échec Graph (cas 2.2.1). Si 8.2 `nb_sans_alerte` élevé
avec `user_id` non null : skip soi ou visibilité. Croiser 8.4 avec
`tache_suivi_personnes` / `createur_id`.

---

## GARDE-FOUS PHASE 2 (rappel)

- Ne pas modifier `FactureCommentaireService` ni le no-op dispatcher
  partagé tant qu’on ne veut pas de Teams sur les factures.
- Ne pas refondre `AlerteService` / page `/alertes`.
- Ne pas envoyer Teams depuis CLI, cron, ou job hors PHP-FPM.
- Pas de migration (scénario et tables déjà là).

## LEÇON

Deux circuits « mention » coexistent : cloche via `AlerteService`
(facture **sans** filtre d’accès, tâche **avec** `estVisiblePar`) et
Teams via `TeamsNotificationService` (offres/juridique/assignation).
Le `MentionNotificationDispatcher` est un crochet mort. Un placeholder
« @ pour mentionner » n’implique pas un Teams. Vérifier d’abord pivot
vs `alertes` avant de « réparer un envoi » qui n’a jamais eu lieu.

---

## DÉPLOIEMENT / TEST

Diagnostic : pas de déploiement fonctionnel. PHASE 2 : `git pull`,
`optimize:clear`, `view:clear` ; pas de migrate ; `journal:sync` si
entrée journal mise à jour.
