Checkpoint: Implémentation de l'envoi automatique des attestations : création d'un scheduler qui vérifie quotidiennement (à 10h00) les séquences terminées la veille et envoie automatiquement les attestations par email pour les formations ayant le mode d'envoi automatique activé (modeEnvoi='auto'). Le système utilise le template email personnalisable créé précédemment et enregistre les envois dans les logs de notifications pour éviter les doublons. Déployé et testé sur le VPS.

This commit is contained in:
Manus
2026-01-30 04:37:31 -05:00
parent 26a16327cf
commit dd581594b5
2 changed files with 211 additions and 0 deletions

View File

@@ -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<boolean> {
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();

View File

@@ -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<boolean> {
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<Date>`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')}`);
}