Checkpoint: Amélioration système de rappels - Historique et notifications d'échec

**Nouvelles fonctionnalités** :

1. **Historique des rappels envoyés**
   - Nouvelle table `logsRappels` pour tracer tous les emails de rappels
   - Enregistrement automatique de chaque envoi (succès ou échec)
   - Prévention des doublons : vérification avant envoi
   - Informations stockées : rappelId, sequenceId, apprenantId, email, type, statut, message d'erreur

2. **Notifications automatiques à l'administrateur**
   - Alerte par email en cas d'échec d'envoi de rappels
   - Détail des échecs : liste des emails et messages d'erreur
   - Résumé : nombre d'envois réussis vs échecs

3. **Procédure de test manuel** (backend uniquement)
   - Nouvelle procédure tRPC `rappels.testerRappel`
   - Permet de tester l'envoi d'un rappel J-7 ou J-1 pour une séquence
   - Envoie au premier apprenant inscrit uniquement

**Fichiers créés** :
- `server/rappelDb.ts` : Fonctions de gestion des logs de rappels
- `drizzle/schema.ts` : Ajout de la table `logsRappels`

**Fichiers modifiés** :
- `server/rappelScheduler.ts` : Intégration du logging et des notifications
- `server/routers.ts` : Ajout de la procédure `testerRappel`

**Bénéfices** :
-  Aucun doublon même en cas de redémarrage du serveur
-  Traçabilité complète de tous les rappels envoyés
-  Alertes automatiques en cas de problème d'envoi
-  Possibilité de tester les rappels avant envoi automatique
This commit is contained in:
Manus Sandbox
2025-12-08 03:01:50 -05:00
parent 4077c57c49
commit bb0ec9e3b0
9 changed files with 2069 additions and 1 deletions

View File

@@ -2,6 +2,8 @@ import { getDb } from "./db";
import { rappels, sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema";
import { eq, and, gte, lte, sql } from "drizzle-orm";
import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService";
import { logRappelEnvoye, rappelDejaEnvoye } from "./rappelDb";
import { notifyOwner } from "./_core/notification";
/**
* Job automatique qui vérifie quotidiennement les séquences à venir
@@ -124,9 +126,20 @@ async function processRappel(rappel: any) {
console.log(`[Rappels] ${inscrits.length} apprenant(s) inscrit(s) à la séquence ${sequence.nom}`);
// Envoyer le rappel à chaque apprenant
let nbEnvoyes = 0;
let nbEchecs = 0;
const echecs: Array<{ email: string; erreur: string }> = [];
for (const { apprenant } of inscrits) {
if (!apprenant || !apprenant.email) continue;
// Vérifier si le rappel a déjà été envoyé
const dejaEnvoye = await rappelDejaEnvoye(rappel.id, sequence.id, apprenant.id);
if (dejaEnvoye) {
console.log(`[Rappels] Rappel déjà envoyé à ${apprenant.email} pour la séquence ${sequence.nom} - ignoré`);
continue;
}
try {
// Choisir la fonction d'envoi selon le type de rappel
if (rappel.templateType === "rappel") {
@@ -164,10 +177,48 @@ async function processRappel(rappel: any) {
});
console.log(`[Rappels] Email J-1 envoyé à ${apprenant.email} pour la séquence ${sequence.nom}`);
}
} catch (emailError) {
// Logger le succès
await logRappelEnvoye({
rappelId: rappel.id,
sequenceId: sequence.id,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
typeRappel: rappel.templateType,
statut: "success",
dateSequence: dates[0].dateDebut,
});
nbEnvoyes++;
} catch (emailError: any) {
const messageErreur = emailError?.message || String(emailError);
console.error(`[Rappels] Erreur lors de l'envoi à ${apprenant.email}:`, emailError);
// Logger l'échec
await logRappelEnvoye({
rappelId: rappel.id,
sequenceId: sequence.id,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
typeRappel: rappel.templateType,
statut: "failed",
messageErreur: messageErreur,
dateSequence: dates[0].dateDebut,
});
nbEchecs++;
echecs.push({ email: apprenant.email, erreur: messageErreur });
}
}
// Notifier l'administrateur en cas d'échecs
if (nbEchecs > 0) {
const listeEchecs = echecs.map(e => `- ${e.email}: ${e.erreur}`).join("\n");
await notifyOwner({
title: `⚠️ Échecs d'envoi de rappels`,
content: `${nbEchecs} rappel(s) n'ont pas pu être envoyés pour la séquence "${sequence.nom}" (${formation.nom}):\n\n${listeEchecs}\n\nRappel: ${rappel.nom} (J-${rappel.joursAvant})\nEnvois réussis: ${nbEnvoyes}`,
});
}
console.log(`[Rappels] Séquence ${sequence.nom}: ${nbEnvoyes} envoi(s) réussi(s), ${nbEchecs} échec(s)`);
}
// Mettre à jour la date de dernière exécution