Files
formation-manager-itinova/server/rappelRetry.ts
Manus 92362ec1d2 Checkpoint: Checkpoint saved: Correction complète de la cohérence des types de rappels entre l'interface et le backend :
**Modifications du schéma Drizzle :**
- logsrappels.type : enum('rappel1','rappel2','rappel3','rappel4','rappel5','rappel6')
- emailTemplates.type : enum('inscription','teaser','rappel1','rappel2','rappel3','rappel4','rappel5','rappel6','reset_password')
- rappels.templateType : enum('rappel1','rappel2','rappel3','rappel4','rappel5','rappel6')

**Modifications du code :**
- server/routers.ts : mise à jour des enums Zod
- server/db.ts : renommage des templates par défaut (rappel → rappel1, rappelJ1 → rappel2)
- server/rappelRetry.ts : correction de la condition rappelJ1 → rappel2
- drizzle/schema.ts : mise à jour de tous les enums

**Migration de la base de données MySQL :**
- Script SQL créé et exécuté avec succès sur le VPS
- Migration en 3 étapes : élargir enum → mettre à jour données → restreindre enum
- Toutes les données existantes migrées correctement

**Résultat :**
-  Plus d'erreur "Unknown column 'type'" dans les logs
-  Cohérence parfaite entre l'interface (Rappel 1, Rappel 2) et le backend (rappel1, rappel2)
-  Système de rappels fonctionnel
-  Déployé et testé sur le VPS de production (https://formations.itinova.org)
2026-01-19 06:39:00 -05:00

227 lines
7.5 KiB
TypeScript

import { getDb } from "./db";
import { logsRappels, sequences, inscriptions, apprenants, formations, datesFormation, rappels, formateurs } from "../drizzle/schema";
import { eq, and } from "drizzle-orm";
import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService";
import { getLogsAReessayer } from "./rappelDb";
import { notifyOwner } from "./_core/notification";
/**
* Calcule le délai avant le prochain essai selon le nombre de tentatives
* Délai exponentiel : 1h, 4h, 24h
*/
function calculerProchainEssai(nbTentatives: number): Date {
const maintenant = new Date();
let delaiHeures = 1; // 1 heure par défaut
if (nbTentatives === 1) {
delaiHeures = 1; // 1ère tentative échouée → réessayer dans 1h
} else if (nbTentatives === 2) {
delaiHeures = 4; // 2ème tentative échouée → réessayer dans 4h
} else {
delaiHeures = 24; // 3ème tentative échouée → réessayer dans 24h (mais on arrête à 3)
}
maintenant.setHours(maintenant.getHours() + delaiHeures);
return maintenant;
}
/**
* Réessaye d'envoyer les rappels échoués
*/
export async function processRappelRetry() {
console.log("[Rappels Retry] Démarrage du processus de réessai");
const db = await getDb();
if (!db) {
console.error("[Rappels Retry] Base de données non disponible");
return;
}
try {
// Récupérer les logs à réessayer
const logsAReessayer = await getLogsAReessayer();
if (logsAReessayer.length === 0) {
console.log("[Rappels Retry] Aucun rappel à réessayer");
return;
}
console.log(`[Rappels Retry] ${logsAReessayer.length} rappel(s) à réessayer`);
let nbSucces = 0;
let nbEchecs = 0;
for (const log of logsAReessayer) {
try {
// Récupérer les informations de la séquence
const sequence = await db
.select()
.from(sequences)
.where(eq(sequences.id, log.sequenceId))
.limit(1);
if (sequence.length === 0) {
console.warn(`[Rappels Retry] Séquence ${log.sequenceId} introuvable`);
continue;
}
const seq = sequence[0];
// Récupérer la formation
const formation = await db
.select()
.from(formations)
.where(eq(formations.id, seq.formationId))
.limit(1);
if (formation.length === 0) {
console.warn(`[Rappels Retry] Formation ${seq.formationId} introuvable`);
continue;
}
// Récupérer l'apprenant
const apprenant = await db
.select()
.from(apprenants)
.where(eq(apprenants.id, log.apprenantId))
.limit(1);
if (apprenant.length === 0 || !apprenant[0].email) {
console.warn(`[Rappels Retry] Apprenant ${log.apprenantId} introuvable ou sans email`);
continue;
}
// Récupérer les dates de la séquence
const dates = await db
.select()
.from(datesFormation)
.where(eq(datesFormation.sequenceId, seq.id))
.orderBy(datesFormation.ordre);
// Récupérer le formateur si disponible
let formateurNom = "";
if (seq.formateurId) {
const formateur = await db
.select()
.from(formateurs)
.where(eq(formateurs.id, seq.formateurId))
.limit(1);
if (formateur.length > 0) {
formateurNom = formateur[0].nom;
}
}
// Réessayer l'envoi
try {
if (log.type === "rappel") {
await sendRappelJ7Email({
apprenantEmail: apprenant[0].email,
apprenantPrenom: apprenant[0].prenom,
apprenantNom: apprenant[0].nom,
apprenantFonction: apprenant[0].fonction,
formationNom: formation[0].nom,
sequenceNom: seq.nom,
dates: dates.map(d => ({
dateDebut: d.dateDebut,
dateFin: d.dateFin,
ordre: d.ordre
})),
lieu: seq.lieu || "",
});
} else if (log.type === "rappel2") {
await sendRappelJ1Email({
apprenantEmail: apprenant[0].email,
apprenantPrenom: apprenant[0].prenom,
apprenantNom: apprenant[0].nom,
apprenantFonction: apprenant[0].fonction,
formationNom: formation[0].nom,
sequenceNom: seq.nom,
dates: dates.map(d => ({
dateDebut: d.dateDebut,
dateFin: d.dateFin,
ordre: d.ordre
})),
lieu: seq.lieu || "",
});
}
// Succès : mettre à jour le log
await db
.update(logsRappels)
.set({
statut: "succes",
nbTentatives: log.nbTentatives + 1,
prochainEssai: null,
})
.where(eq(logsRappels.id, log.id));
console.log(`[Rappels Retry] Succès pour ${apprenant[0].email} après ${log.nbTentatives + 1} tentative(s)`);
nbSucces++;
} catch (emailError: any) {
const messageErreur = emailError?.message || String(emailError);
const nouvelleTentative = log.nbTentatives + 1;
if (nouvelleTentative >= 3) {
// Abandon après 3 tentatives
await db
.update(logsRappels)
.set({
statut: "echec",
nbTentatives: nouvelleTentative,
messageErreur: `Abandon après 3 tentatives: ${messageErreur}`,
prochainEssai: null,
})
.where(eq(logsRappels.id, log.id));
console.error(`[Rappels Retry] Abandon pour ${apprenant[0].email} après 3 tentatives`);
// Notifier l'admin
await notifyOwner({
title: `❌ Abandon d'envoi de rappel`,
content: `Le rappel pour ${apprenant[0].email} (${seq.nom}) a échoué 3 fois et a été abandonné.\\n\\nDernière erreur: ${messageErreur}`,
});
} else {
// Planifier un nouvel essai
const prochainEssai = calculerProchainEssai(nouvelleTentative);
await db
.update(logsRappels)
.set({
nbTentatives: nouvelleTentative,
messageErreur: messageErreur,
prochainEssai: prochainEssai,
})
.where(eq(logsRappels.id, log.id));
console.log(`[Rappels Retry] Échec pour ${apprenant[0].email}, tentative ${nouvelleTentative}/3. Prochain essai: ${prochainEssai.toLocaleString()}`);
}
nbEchecs++;
}
} catch (error) {
console.error(`[Rappels Retry] Erreur lors du traitement du log ${log.id}:`, error);
}
}
console.log(`[Rappels Retry] Traitement terminé: ${nbSucces} succès, ${nbEchecs} échecs`);
} catch (error) {
console.error("[Rappels Retry] Erreur lors du processus de retry:", error);
}
}
/**
* Initialise le scheduler de retry
* Vérifie toutes les heures s'il y a des rappels à réessayer
*/
export function initRappelRetryScheduler() {
console.log("[Rappels Retry] Initialisation du scheduler de réessai automatique");
// Exécuter immédiatement au démarrage
processRappelRetry();
// Puis exécuter toutes les heures
setInterval(() => {
processRappelRetry();
}, 60 * 60 * 1000); // 1 heure en millisecondes
console.log("[Rappels Retry] Scheduler de réessai initialisé - vérification toutes les heures");
}