Notifications

Module notifications — catalogue d'événements, emails transactionnels, configuration des providers, file d'attente Prisma et politique de retry.

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

ConstanteValeur
AUTH_EMAIL_VERIFICATION_REQUESTEDauth.email_verification.requested
AUTH_EMAIL_VERIFIEDauth.email_verification.completed
AUTH_PASSWORD_RESET_REQUESTEDauth.password_reset.requested
AUTH_PASSWORD_RESET_COMPLETEDauth.password_reset.completed
AUTH_TWO_FACTOR_OTP_REQUESTEDauth.two_factor.otp_requested
AUTH_MAGIC_LINK_REQUESTEDauth.magic_link.requested

KYB

ConstanteValeur
KYB_SUBMITTEDkyb.submitted
KYB_IN_REVIEWkyb.in_review
KYB_APPROVEDkyb.approved
KYB_REJECTEDkyb.rejected

Réservation

ConstanteValeur
RESERVATION_CREATEDreservation.created
RESERVATION_PENDING_DISTRIBUTOR_APPROVALreservation.pending_distributor_approval
RESERVATION_APPROVEDreservation.approved
RESERVATION_REJECTEDreservation.rejected
RESERVATION_CANCELLEDreservation.cancelled

Facturation

ConstanteValeur
BILLING_INVOICE_ISSUEDbilling.invoice.issued
BILLING_INVOICE_PAIDbilling.invoice.paid
BILLING_PAYMENT_FAILEDbilling.payment.failed
BILLING_CREDIT_NOTE_ISSUEDbilling.credit_note.issued

Logistique

ConstanteValeur
LOGISTICS_PLATE_PREPARATION_STARTEDlogistics.plate_preparation.started
LOGISTICS_PLATE_SHIPPEDlogistics.plate.shipped
LOGISTICS_PLATE_DELIVEREDlogistics.plate.delivered
LOGISTICS_PLATE_RETURN_REQUESTEDlogistics.plate.return_requested

Meubles (demandes de meuble distributeur)

ConstanteValeurDestinataire
FURNITURE_REQUEST_CREATEDfurniture_request.createdAdmins plateforme
FURNITURE_REQUEST_APPROVEDfurniture_request.approvedMembres du distributeur
FURNITURE_REQUEST_REJECTEDfurniture_request.rejectedMembres 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_*.

VariableValeurs / remarques
EMAIL_PROVIDERscaleway, resend ou devlog
EMAIL_FROMIdentité d'expéditeur (requis)
EMAIL_REPLY_TOAdresse de réponse (optionnel)
RESEND_API_KEYRequis si EMAIL_PROVIDER=resend hors développement
SMTP_HOSTRequis si EMAIL_PROVIDER=scaleway
SMTP_PORTRequis si EMAIL_PROVIDER=scaleway
SMTP_SECUREtrue (SSL/TLS) ou false (STARTTLS)
SMTP_USERNAMEProject ID Scaleway TEM
SMTP_PASSWORDClé secrète IAM Scaleway TEM

Logique de résolution du provider

  1. Si EMAIL_PROVIDER est explicite, il est utilisé.
  2. Sinon, si RESEND_API_KEY est défini, le provider résout à resend.
  3. Si resend est sélectionné sans clé API, ou scaleway sans SMTP complet, le runtime bascule sur devlog en 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 : 587 ou 2587 (SMTP_SECURE=false)
  • Ports SSL/TLS : 465 ou 2465 (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), puis quarantine / 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

ChampUtilisé par
recipientNamesalutation personnalisée
actionUrlbouton CTA et lien de secours
reservationReferencetemplates reservation.*
invoiceReferencetemplates billing.*
amountbilling.invoice.issued
kybReasonkyb.rejected
otpCodeauth.two_factor.otp_requested
logisticsCarrierlogistics.plate.shipped
logisticsTrackingNumberlogistics.plate.shipped
logisticsTrackingUrllogistics.plate.shipped
printOrderCodelogistics.plate.shipped
furnitureRequestReferencetemplates furniture_request.*
furnitureRequestReasonfurniture_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)

ColonneTypeDescription
idString (cuid)Identifiant interne
jobIdString (unique)Identifiant de job
statusStringÉtat courant (queued, …)
toString[]Destinataires
eventStringClé événement
contextJsonContexte de rendu
requestIdStringCorrélation avec la requête source
attemptsIntNombre de tentatives effectuées
maxAttemptsIntSeuil maximal
nextAttemptAtDateTimeProchaine tentative
lockedAtDateTime?Verrouillage worker
providerMessageIdString?ID renvoyé par le provider
lastErrorString?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

VariableValeur par défautDescription
EMAIL_QUEUE_ENABLEDtrueActive ou désactive le worker
EMAIL_QUEUE_POLL_INTERVAL_MS5000Intervalle de polling en ms
EMAIL_QUEUE_MAX_ATTEMPTS5Nombre maximal de tentatives avant dead-letter
EMAIL_QUEUE_RETRY_BASE_DELAY_MS15000Base 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 :

  1. Déclencher le flux (inscription, mot de passe oublié, reset admin).
  2. Rechercher local auth email action link generated dans les logs du dev server.
  3. Copier actionUrl et 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_FROM aligné avec le domaine vérifié du provider.
  • En local, utiliser EMAIL_PROVIDER=devlog sauf test d'intégration explicite.