diff --git a/server/_core/index.ts b/server/_core/index.ts index 2f4cca0..3ca2c06 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -15,6 +15,7 @@ import { initRappelScheduler } from "../rappelScheduler"; import { initRappelRetryScheduler } from "../rappelRetry"; import { initFail2BanAlertSystem } from "../fail2banAlertService"; import { initAttestationTemplate } from "../initTemplates"; +import { initAttestationScheduler } from "../attestationScheduler"; function isPortAvailable(port: number): Promise { return new Promise(resolve => { @@ -82,6 +83,9 @@ async function startServer() { // Initialiser le scheduler de rappels automatiques initRappelScheduler(); + // Initialiser le scheduler d'envoi automatique des attestations + initAttestationScheduler(); + // Initialiser le scheduler de réessai automatique initRappelRetryScheduler(); diff --git a/server/attestationScheduler.ts b/server/attestationScheduler.ts new file mode 100644 index 0000000..d8f80f6 --- /dev/null +++ b/server/attestationScheduler.ts @@ -0,0 +1,207 @@ +import { getDb } from "./db"; +import { sequences, inscriptions, apprenants, formations, datesFormation, attestations, logsNotifications } from "../drizzle/schema"; +import { eq, and, lte, sql, isNull } from "drizzle-orm"; +import { sendAttestationEmail } from "./emailService"; +import { logNotification } from "./notificationLogsDb"; + +/** + * Vérifie si une attestation a déjà été envoyée pour une inscription + */ +async function attestationDejaEnvoyee(apprenantId: number, sequenceId: number): Promise { + const db = await getDb(); + if (!db) return false; + + try { + const [existing] = await db + .select({ id: logsNotifications.id }) + .from(logsNotifications) + .where( + and( + eq(logsNotifications.type, "attestation"), + eq(logsNotifications.apprenantId, apprenantId), + eq(logsNotifications.sequenceId, sequenceId), + eq(logsNotifications.statut, "success") + ) + ) + .limit(1); + + return !!existing; + } catch (error) { + console.error("[Attestations Auto] Erreur lors de la vérification du log:", error); + return false; + } +} + +/** + * Job automatique qui vérifie quotidiennement les séquences terminées + * et envoie les attestations aux apprenants pour les formations en mode envoi automatique + */ +export async function processAttestationsAutomatiques() { + console.log("[Attestations Auto] Démarrage du traitement des attestations automatiques..."); + + const db = await getDb(); + if (!db) { + console.error("[Attestations Auto] Base de données non disponible"); + return; + } + + try { + const now = new Date(); + const hier = new Date(); + hier.setDate(hier.getDate() - 1); + hier.setHours(23, 59, 59, 999); + + // Récupérer les séquences dont la dernière date est passée (terminées hier) + // ET dont la formation a le mode d'envoi automatique activé + const sequencesTerminees = await db + .select({ + sequence: sequences, + formation: formations, + derniereDate: sql`MAX(${datesFormation.dateFin})`.as('derniereDate'), + }) + .from(sequences) + .innerJoin(formations, eq(sequences.formationId, formations.id)) + .innerJoin(datesFormation, eq(sequences.id, datesFormation.sequenceId)) + .where( + and( + eq(formations.modeEnvoi, 'auto'), // Seulement les formations en mode envoi automatique + eq(sequences.statut, 'ouverte') // Séquences encore ouvertes + ) + ) + .groupBy(sequences.id, formations.id) + .having(lte(sql`MAX(${datesFormation.dateFin})`, hier)); + + console.log(`[Attestations Auto] ${sequencesTerminees.length} séquence(s) terminée(s) avec envoi automatique trouvée(s)`); + + for (const { sequence, formation, derniereDate } of sequencesTerminees) { + await processAttestationSequence(sequence, formation, derniereDate); + } + + console.log("[Attestations Auto] Traitement terminé"); + } catch (error) { + console.error("[Attestations Auto] Erreur lors du traitement des attestations:", error); + } +} + +async function processAttestationSequence( + sequence: any, + formation: any, + derniereDate: Date +) { + console.log(`[Attestations Auto] Traitement de la séquence: ${sequence.nom} (terminée le ${derniereDate})`); + + const db = await getDb(); + if (!db) return; + + try { + // Récupérer les inscriptions confirmées avec leurs attestations + const inscriptionsAvecAttestations = await db + .select({ + inscription: inscriptions, + apprenant: apprenants, + attestation: attestations, + }) + .from(inscriptions) + .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) + .leftJoin(attestations, eq(inscriptions.id, attestations.inscriptionId)) + .where( + and( + eq(inscriptions.sequenceId, sequence.id), + eq(inscriptions.statut, 'confirmee') + ) + ); + + console.log(`[Attestations Auto] ${inscriptionsAvecAttestations.length} inscription(s) confirmée(s) pour ${sequence.nom}`); + + let envoyesCount = 0; + let errorsCount = 0; + let sansAttestationCount = 0; + + for (const { inscription, apprenant, attestation } of inscriptionsAvecAttestations) { + // Vérifier si l'attestation existe + if (!attestation || !attestation.documentUrl) { + sansAttestationCount++; + console.log(`[Attestations Auto] Pas d'attestation générée pour ${apprenant.email}`); + continue; + } + + // Vérifier si l'attestation a déjà été envoyée + const dejaEnvoyee = await attestationDejaEnvoyee(apprenant.id, sequence.id); + if (dejaEnvoyee) { + console.log(`[Attestations Auto] Attestation déjà envoyée pour ${apprenant.email}`); + continue; + } + + try { + await sendAttestationEmail({ + to: apprenant.email, + apprenantNom: apprenant.nom, + apprenantPrenom: apprenant.prenom, + formationNom: formation.nom, + pdfUrl: attestation.documentUrl, + }); + + // Logger la notification + await logNotification({ + type: "attestation", + sequenceId: sequence.id, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + sujet: `Votre attestation de formation - ${formation.nom}`, + statut: "success", + metadata: { attestationId: attestation.id }, + }); + + envoyesCount++; + console.log(`[Attestations Auto] Email envoyé à ${apprenant.email}`); + } catch (error: any) { + // Logger l'échec + await logNotification({ + type: "attestation", + sequenceId: sequence.id, + apprenantId: apprenant.id, + emailDestinataire: apprenant.email, + sujet: `Votre attestation de formation - ${formation.nom}`, + statut: "failed", + messageErreur: error.message, + }); + + errorsCount++; + console.error(`[Attestations Auto] Erreur envoi à ${apprenant.email}:`, error); + } + } + + console.log(`[Attestations Auto] Séquence ${sequence.nom}: ${envoyesCount} envoyé(s), ${errorsCount} erreur(s), ${sansAttestationCount} sans attestation`); + + } catch (error) { + console.error(`[Attestations Auto] Erreur pour la séquence ${sequence.nom}:`, error); + } +} + +/** + * Initialise le scheduler pour l'envoi automatique des attestations + * Exécute le job tous les jours à 10h00 + */ +export function initAttestationScheduler() { + console.log("[Attestations Auto] Initialisation du scheduler d'envoi automatique"); + + // Exécuter immédiatement au démarrage + processAttestationsAutomatiques(); + + // Puis exécuter tous les jours à 10h00 + const HEURE_EXECUTION = 10; // 10h00 + const MINUTE_EXECUTION = 0; + + setInterval(() => { + const now = new Date(); + const heureActuelle = now.getHours(); + const minuteActuelle = now.getMinutes(); + + // Exécuter seulement à l'heure prévue (avec une tolérance de 1 minute) + if (heureActuelle === HEURE_EXECUTION && minuteActuelle === MINUTE_EXECUTION) { + processAttestationsAutomatiques(); + } + }, 60 * 1000); // Vérifier toutes les minutes + + console.log(`[Attestations Auto] Scheduler initialisé - Exécution quotidienne à ${HEURE_EXECUTION}:${String(MINUTE_EXECUTION).padStart(2, '0')}`); +}