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.typeRappel === "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.typeRappel === "rappelJ1") { 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: "success", 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: "failed", 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"); }