/** * Scheduler für automatische Geburtstagsgrüße. * * Läuft täglich um 08:00 Uhr und sendet Grüße an alle Kunden mit: * - Geburtstag = heute * - autoBirthdayGreeting = true * - autoBirthdayChannel ist gesetzt (aktuell nur 'email' automatisiert) * - lastBirthdayGreetingYear != aktuelles Jahr (verhindert Doppel-Versand) */ import cron from 'node-cron'; import prisma from '../lib/prisma.js'; import { sendEmail, SmtpCredentials } from './smtpService.js'; import { getSystemEmailCredentials } from './emailProvider/emailProviderService.js'; import * as birthdayService from './birthday.service.js'; async function runDailyBirthdayGreetings(): Promise { const today = new Date(); today.setHours(0, 0, 0, 0); const thisYear = today.getFullYear(); const month = today.getMonth() + 1; // Prisma-Raw-SQL ist 1-indexed const day = today.getDate(); console.log( `[BirthdayScheduler] Suche Kunden mit Geburtstag ${day}.${month}., Auto-Versand aktiv …`, ); // Kunden mit heutigem Geburtstag + Auto-Versand + dieses Jahr noch nicht gesendet const candidates = await prisma.$queryRaw< Array<{ id: number; firstName: string; lastName: string; email: string | null; salutation: string | null; useInformalAddress: boolean; birthDate: Date; autoBirthdayChannel: string | null; }> >` SELECT id, firstName, lastName, email, salutation, useInformalAddress, birthDate, autoBirthdayChannel FROM Customer WHERE autoBirthdayGreeting = 1 AND birthDate IS NOT NULL AND MONTH(birthDate) = ${month} AND DAY(birthDate) = ${day} AND (lastBirthdayGreetingYear IS NULL OR lastBirthdayGreetingYear != ${thisYear}) `; if (candidates.length === 0) { console.log('[BirthdayScheduler] Keine passenden Kunden heute.'); return; } console.log(`[BirthdayScheduler] ${candidates.length} Kunde(n) gefunden – sende Grüße.`); // System-E-Mail-Credentials einmal laden const systemEmail = await getSystemEmailCredentials(); if (!systemEmail) { console.error( '[BirthdayScheduler] Keine System-E-Mail konfiguriert – kann keine Grüße versenden.', ); return; } const smtpCreds: SmtpCredentials = { host: systemEmail.smtpServer, port: systemEmail.smtpPort, user: systemEmail.emailAddress, password: systemEmail.password, encryption: systemEmail.smtpEncryption, allowSelfSignedCerts: systemEmail.allowSelfSignedCerts, }; let sent = 0; let skipped = 0; for (const c of candidates) { const channel = c.autoBirthdayChannel || 'email'; // Aktuell nur Email automatisch – Messenger brauchen Browser-Klick if (channel !== 'email') { console.log( `[BirthdayScheduler] Kunde #${c.id} (${c.firstName} ${c.lastName}): Kanal "${channel}" nicht automatisierbar, übersprungen.`, ); skipped++; continue; } if (!c.email) { console.log( `[BirthdayScheduler] Kunde #${c.id} (${c.firstName} ${c.lastName}): keine E-Mail hinterlegt, übersprungen.`, ); skipped++; continue; } const age = thisYear - new Date(c.birthDate).getFullYear(); const { subject, html } = birthdayService.buildBirthdayGreetingText( { firstName: c.firstName, lastName: c.lastName, salutation: c.salutation, useInformalAddress: c.useInformalAddress, }, age, ); try { const result = await sendEmail( smtpCreds, systemEmail.emailAddress, { to: c.email, subject, html }, { context: 'birthday-greeting-auto', customerId: c.id, triggeredBy: 'cron' }, ); if (result.success) { // Marker setzen damit nächstes Jahr wieder läuft, dieses Jahr aber nicht nochmal await prisma.customer.update({ where: { id: c.id }, data: { lastBirthdayGreetingYear: thisYear }, }); sent++; console.log( `[BirthdayScheduler] ✓ Kunde #${c.id} (${c.firstName} ${c.lastName}): Gruß gesendet.`, ); } else { console.error( `[BirthdayScheduler] ✗ Kunde #${c.id}: Sendfehler: ${result.error}`, ); skipped++; } } catch (err) { console.error(`[BirthdayScheduler] ✗ Kunde #${c.id}: Exception:`, err); skipped++; } } console.log( `[BirthdayScheduler] Fertig: ${sent} versendet, ${skipped} übersprungen von ${candidates.length} Kandidaten.`, ); } /** * Scheduler starten. Läuft täglich um 08:00 in lokaler Server-Zeit. * Zusätzlich: ein Test-Lauf 30 Sekunden nach Server-Start, aber nur wenn heute schon jemand Geburtstag hat * (sonst passiert eh nichts). So können wir bei Ausfall am Tag X direkt beim nächsten Boot nachholen. */ export function startBirthdayScheduler(): void { // Täglich um 08:00 cron.schedule('0 8 * * *', () => { runDailyBirthdayGreetings().catch((err) => console.error('[BirthdayScheduler] Daily run failed:', err), ); }); // Einmal 30 Sekunden nach Start (Catch-up bei Ausfall) setTimeout(() => { runDailyBirthdayGreetings().catch((err) => console.error('[BirthdayScheduler] Catch-up run failed:', err), ); }, 30_000); console.log('[BirthdayScheduler] Gestartet – täglich um 08:00 + Catch-up nach 30s'); } /** * Für manuelles Triggern (z.B. aus Debug-Endpoint). */ export { runDailyBirthdayGreetings };