ADR-0013 — Notifications e-mail (workflow Demande)¶
Statut : accepté (2026-06-15, arbitrages user).
Contexte : le workflow Demande (Jalon 3, ADR-0006) est une machine à états
soumis → assignee → en_etude → revue_senior → validee → audit_externe → livre (+ renvois).
Chaque transition est déjà journalisée (EvenementDemande), mais personne n'est
prévenu : le senior ne sait pas qu'une demande est arrivée, le junior qu'on lui a
assigné une étude, le client que son étude est livrée. Aucune infra mail n'existait
(l'e-mail ne servait qu'au match d'identité SSO). Périmètre validé : carte complète
(transitions internes + e-mail au client).
🎯 À retenir
Chaque transition du workflow enfile une notification dans une outbox (lignes
Notification persistées dans la même transaction que l'événement) — jamais d'envoi
bloquant dans la transition. L'envoi est un acte séparé (drainer_outbox), best-effort,
désactivé par défaut ; idempotent (dedup_cle) et sans fuite du commentaire interne
au client.
La question¶
Brancher des envois sur des transitions, sans (a) qu'un mail puisse faire échouer une transition, (b) qu'un mail réel parte par accident en dev/test, (c) fuiter la cuisine de revue interne au client, ni (d) doubler un mail sur un rejeu.
Décisions¶
| # | Décision | Arbitrage |
|---|---|---|
| D-NOTIF-1 | Politique déclarative sur le point de passage unique. Toutes les transitions passent par journaliser ; un wrapper _tracer journalise et enfile. Une table POLITIQUE mappe type d'événement → (résolveur de destinataire, gabarit) — seuls les types mappés notifient (démarrage, accès cross-tenant… restent muets). |
un seul hook, déclaratif |
| D-NOTIF-2 | Outbox, pas d'envoi dans la transition. enfiler PERSISTE des lignes Notification (en_attente) dans la même transaction que l'événement — une transition ne peut jamais échouer à cause d'un mail. L'envoi est un acte SÉPARÉ (drainer_outbox), best-effort. |
robustesse |
| D-NOTIF-3 | notif_enabled = False par défaut. Tant que c'est faux (dev, test, SMTP non configuré), l'outbox se remplit mais rien ne part — zéro mail réel par accident. En prod, on passe à True et un cron appelle POST /api/notifications/drainer. |
sécurité du sortant |
| D-NOTIF-4 | E-mail client FIGÉ à la soumission (Demande.created_by_email, depuis la session SSO, en minuscules) — jamais résolu depuis un claim au moment de l'envoi. On notifie l'adresse qui a sollicité. Destinataires staff résolus depuis StaffActuaire (assigné, externe, seniors). |
auditabilité |
| D-NOTIF-5 | Le commentaire interne reste au journal, jamais dans l'e-mail : le mail porte une intro fixe + un lien vers la demande. Pas de fuite des critiques de revue / réserves d'audit vers le client. | cloison interne/client |
| D-NOTIF-6 | Idempotence + immuabilité. dedup_cle unique (demande, type, destinataire) : un rejeu de transition ne double pas le mail. Jamais de DELETE (journal d'audit des communications). Drain réservé au staff. |
rejeu sûr |
Carte des notifications¶
| Transition | Destinataire | Résolveur |
|---|---|---|
soumission (création) |
seniors | _seniors |
assignation |
junior assigné | assignee_staff_id |
soumission_revue |
seniors | _seniors |
critique_revue (renvoi) |
junior | assignee_staff_id |
validation_senior |
junior (bonne nouvelle) | assignee_staff_id |
transmission_externe |
auditeur externe | externe_staff_id |
audit_approuve / audit_reserves |
seniors | _seniors |
livraison |
client | created_by_email |
Mécanique¶
app/notifications.py : la POLITIQUE, enfiler(db, demande, type) (persistance
outbox, idempotente), drainer_outbox(db) (SMTP best-effort, no-op si désactivé), et
deux routes staff — GET /api/notifications (visibilité de l'outbox) et
POST /api/notifications/drainer. Hooks : creer_demande / creer_demande_pour_compte
(événement soumission) et _tracer dans workflow.py (toutes les transitions).
Config ACTUARYLAB_SMTP_* + ACTUARYLAB_NOTIF_ENABLED (cf. .env.example).
Conséquences¶
- + Chaque acteur est prévenu au bon moment ; le client est tenu informé de la livraison — sans qu'un incident SMTP ne casse jamais une transition métier.
- + Testable sans réseau : on asserte les lignes d'outbox (désactivé) puis l'envoi via un transport SMTP factice (6 tests).
- −/assumé Pas d'envoi automatique : en prod un cron doit appeler le drain (cohérent avec l'outbox ; un worker/BackgroundTask est une évolution possible).
⚠️ Piège
En prod, rien ne part tant que ACTUARYLAB_NOTIF_ENABLED n'est pas passé à True
et qu'un cron n'appelle pas POST /api/notifications/drainer. L'outbox se remplit
silencieusement sinon — aucun mail réel, aucune erreur visible.
- Évolutions : préférences de notification par utilisateur, digest, gabarits HTML, vue outbox côté front (l'endpoint existe déjà).
Voir mémoire projet. S'appuie sur le workflow d'audit en chaîne (ADR-0006).