Notifications
Notifications
Le module notifications gère l'envoi d'emails transactionnels déclenchés par les événements métier de la plateforme. Il expose un catalogue d'événements typés, un système de rendu de templates, une file d'attente persistante PostgreSQL et un worker de retry à backoff exponentiel.
Chemin : app/mypromo/server/modules/notifications/
Catalogue d'événements
Source de vérité : app/mypromo/server/modules/notifications/domain/notification-events.ts
Les événements sont organisés par domaine avec un nommage en points (domain.entite.action).
Auth
| Constante | Valeur |
|---|---|
AUTH_EMAIL_VERIFICATION_REQUESTED | auth.email_verification.requested |
AUTH_EMAIL_VERIFIED | auth.email_verification.completed |
AUTH_PASSWORD_RESET_REQUESTED | auth.password_reset.requested |
AUTH_PASSWORD_RESET_COMPLETED | auth.password_reset.completed |
AUTH_TWO_FACTOR_OTP_REQUESTED | auth.two_factor.otp_requested |
AUTH_MAGIC_LINK_REQUESTED | auth.magic_link.requested |
KYB
| Constante | Valeur |
|---|---|
KYB_SUBMITTED | kyb.submitted |
KYB_IN_REVIEW | kyb.in_review |
KYB_APPROVED | kyb.approved |
KYB_REJECTED | kyb.rejected |
Réservation
| Constante | Valeur |
|---|---|
RESERVATION_CREATED | reservation.created |
RESERVATION_PENDING_DISTRIBUTOR_APPROVAL | reservation.pending_distributor_approval |
RESERVATION_APPROVED | reservation.approved |
RESERVATION_REJECTED | reservation.rejected |
RESERVATION_CANCELLED | reservation.cancelled |
Facturation
| Constante | Valeur |
|---|---|
BILLING_INVOICE_ISSUED | billing.invoice.issued |
BILLING_INVOICE_PAID | billing.invoice.paid |
BILLING_PAYMENT_FAILED | billing.payment.failed |
BILLING_CREDIT_NOTE_ISSUED | billing.credit_note.issued |
Logistique
| Constante | Valeur |
|---|---|
LOGISTICS_PLATE_PREPARATION_STARTED | logistics.plate_preparation.started |
LOGISTICS_PLATE_SHIPPED | logistics.plate.shipped |
LOGISTICS_PLATE_DELIVERED | logistics.plate.delivered |
LOGISTICS_PLATE_RETURN_REQUESTED | logistics.plate.return_requested |
Meubles (demandes de meuble distributeur)
| Constante | Valeur | Destinataire |
|---|---|---|
FURNITURE_REQUEST_CREATED | furniture_request.created | Admins plateforme |
FURNITURE_REQUEST_APPROVED | furniture_request.approved | Membres du distributeur |
FURNITURE_REQUEST_REJECTED | furniture_request.rejected | Membres du distributeur |
Émis par app/mypromo/server/modules/catalog/application/furniture-request-notifications.ts, appelé depuis createFurnitureRequest (création) et approveFurnitureRequest/rejectFurnitureRequest (module catalog). Les admins destinataires sont les utilisateurs avec role: 'admin' ; les membres distributeur sont résolus via OrganizationMember (actifs, revokedAt: null) sur l'organisation de la demande.
Règles de nommage
- Noms en points, préfixés par domaine.
- Suffixes stables :
requested,completed,approved,rejected,submitted,in_review,issued,paid,failed,started,shipped,delivered,cancelled. - Les IDs d'événement servent de clé de payload dans la file d'attente et comme dimension dans les métriques de retry.
Configuration des providers
Contrôlé par les variables d'environnement EMAIL_*.
| Variable | Valeurs / remarques |
|---|---|
EMAIL_PROVIDER | scaleway, resend ou devlog |
EMAIL_FROM | Identité d'expéditeur (requis) |
EMAIL_REPLY_TO | Adresse de réponse (optionnel) |
RESEND_API_KEY | Requis si EMAIL_PROVIDER=resend hors développement |
SMTP_HOST | Requis si EMAIL_PROVIDER=scaleway |
SMTP_PORT | Requis si EMAIL_PROVIDER=scaleway |
SMTP_SECURE | true (SSL/TLS) ou false (STARTTLS) |
SMTP_USERNAME | Project ID Scaleway TEM |
SMTP_PASSWORD | Clé secrète IAM Scaleway TEM |
Logique de résolution du provider
- Si
EMAIL_PROVIDERest explicite, il est utilisé. - Sinon, si
RESEND_API_KEYest défini, le provider résout àresend. - Si
resendest sélectionné sans clé API, ouscalewaysans SMTP complet, le runtime bascule surdevlogen développement seulement.
Provider devlog
En mode devlog, les emails ne sont pas envoyés — ils sont loggés en structuré avec les destinataires masqués. C'est le mode recommandé en développement local sauf test d'intégration explicite.
Scaleway TEM — valeurs de référence
- Host SMTP :
smtp.tem.scaleway.com - Ports STARTTLS :
587ou2587(SMTP_SECURE=false) - Ports SSL/TLS :
465ou2465(SMTP_SECURE=true) - Username : Project ID de l'espace Scaleway où le domaine TEM est configuré
- Password : Clé secrète d'une API key IAM avec droit SMTP TEM
DNS en production
Avant activation en production, configurer sur le domaine expéditeur :
- SPF : autoriser les serveurs d'envoi du provider
- DKIM : clés générées par le provider (obligatoire)
- DMARC : commencer en
p=none(monitoring), puisquarantine/reject
Templates d'email
Source de vérité : app/mypromo/server/modules/notifications/email/application/email-templates.ts
Contrat de rendu
renderTransactionalEmailTemplate(event: NotificationEvent, context: TransactionalEmailTemplateContext)
// → { subject: string, text: string, html: string }
Appelé via EmailService.sendTransactionalEmailByEvent(...).
- Entrée :
event(clé du catalogue) +context(données dynamiques). - Sortie :
subject,text(fallback email client sans HTML),html(layout tabulaire responsive).
Champs de contexte principaux
| Champ | Utilisé par |
|---|---|
recipientName | salutation personnalisée |
actionUrl | bouton CTA et lien de secours |
reservationReference | templates reservation.* |
invoiceReference | templates billing.* |
amount | billing.invoice.issued |
kybReason | kyb.rejected |
otpCode | auth.two_factor.otp_requested |
logisticsCarrier | logistics.plate.shipped |
logisticsTrackingNumber | logistics.plate.shipped |
logisticsTrackingUrl | logistics.plate.shipped |
printOrderCode | logistics.plate.shipped |
furnitureRequestReference | templates furniture_request.* |
furnitureRequestReason | furniture_request.rejected |
Template auth.magic_link.requested
Template dédié au lien magique : bouton CTA en orange (#ea580c), carte d'information sécurité inlinée, avertissement de ne pas partager le lien.
Header branding
Le logo partagé pointe vers https://dev-images-mail.s3.fr-par.scw.cloud/pulsemypromo/AppLogo.svg directement (pas d'URL locale) pour la compatibilité avec les clients email mobiles. Le tag d'événement est automatiquement propagé (tags: [{ name: 'event', value: <event> }]).
File d'attente persistante et worker
Modèles Prisma
Table principale : notification_email_queue (modèle EmailQueueItem)
| Colonne | Type | Description |
|---|---|---|
id | String (cuid) | Identifiant interne |
jobId | String (unique) | Identifiant de job |
status | String | État courant (queued, …) |
to | String[] | Destinataires |
event | String | Clé événement |
context | Json | Contexte de rendu |
requestId | String | Corrélation avec la requête source |
attempts | Int | Nombre de tentatives effectuées |
maxAttempts | Int | Seuil maximal |
nextAttemptAt | DateTime | Prochaine tentative |
lockedAt | DateTime? | Verrouillage worker |
providerMessageId | String? | ID renvoyé par le provider |
lastError | String? | Dernière erreur |
Index : (status, nextAttemptAt) — utilisé par le worker pour le polling.
Table de rebut : notification_email_dead_letter (modèle EmailDeadLetter)
Mêmes colonnes qu'EmailQueueItem avec en plus deadLetteredAt (horodatage du passage en dead-letter). Le statut par défaut est dead_letter.
Worker
Plugin Nitro : app/mypromo/server/plugins/20-email-queue-worker.ts
Le worker tourne en tâche de fond dans le processus Nitro. Il interroge la table notification_email_queue à intervalle régulier, acquiert un verrou sur les jobs éligibles et tente l'envoi via le provider configuré.
Variables de contrôle du worker
| Variable | Valeur par défaut | Description |
|---|---|---|
EMAIL_QUEUE_ENABLED | true | Active ou désactive le worker |
EMAIL_QUEUE_POLL_INTERVAL_MS | 5000 | Intervalle de polling en ms |
EMAIL_QUEUE_MAX_ATTEMPTS | 5 | Nombre maximal de tentatives avant dead-letter |
EMAIL_QUEUE_RETRY_BASE_DELAY_MS | 15000 | Base du délai pour le backoff exponentiel |
Politique de retry
Backoff exponentiel basé sur EMAIL_QUEUE_RETRY_BASE_DELAY_MS. Après EMAIL_QUEUE_MAX_ATTEMPTS tentatives échouées, le job est déplacé dans notification_email_dead_letter.
API — smoke test
Authentification — la plateforme utilise des sessions par cookie (Better Auth). La session est établie à la connexion et transmise automatiquement via le cookie
better-auth.session_token. Dans les exemples curl, utilisez-b 'better-auth.session_token=<jeton-de-session>'.
Envoyer un email de smoke test
POST /api/notifications/email-smoke
Content-Type: application/json
Cookie: better-auth.session_token=<jeton-de-session>
{
"to": "destinataire@example.com",
"event": "billing.invoice.issued"
}
Requiert la permission platform.manage (rôle admin).
event est optionnel (défaut : billing.invoice.issued). Il doit correspondre à une valeur valide du catalogue.
{
"actorUserId": "...",
"actorRole": "admin",
"to": "destinataire@example.com",
"event": "billing.invoice.issued",
"queueStatus": "queued",
"jobId": "job_...",
"nextAttemptAt": "2026-06-19T10:00:05.000Z"
}
L'envoi est asynchrone : la réponse confirme la mise en file, le worker envoie sur le prochain cycle de polling.
Développement local — récupérer les liens auth depuis les logs
En développement (NODE_ENV=development), pour les événements auth.email_verification.requested et auth.password_reset.requested, une entrée de log structurée est émise avant l'enfilage dans la queue :
scope: "better-auth"
message: "local auth email action link generated"
fields: eventType, userId, email, actionUrl, requestId
Procédure :
- Déclencher le flux (inscription, mot de passe oublié, reset admin).
- Rechercher
local auth email action link generateddans les logs du dev server. - Copier
actionUrlet l'ouvrir dans le navigateur.
Ce log est strictement local et n'est pas émis en production.
Sécurité
- Ne jamais committer de clés provider réelles.
- Maintenir
EMAIL_FROMaligné avec le domaine vérifié du provider. - En local, utiliser
EMAIL_PROVIDER=devlogsauf test d'intégration explicite.