Aller au contenu

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