diff --git a/backend/lib/expiryJob.js b/backend/lib/expiryJob.js new file mode 100644 index 0000000..5a65181 --- /dev/null +++ b/backend/lib/expiryJob.js @@ -0,0 +1,93 @@ +import cron from 'node-cron'; +import pool from '../config/database.js'; +import { + sendDemoExpiryReminderMail, + sendDemoLastDayMail, + sendDemoExpiredMail, +} from './mail.js'; + +// Findet Demo-Tenants, deren Ablauf in [from, to] liegt UND fuer die noch +// keine entsprechende Benachrichtigung versandt wurde, schickt die Mail +// und markiert die Spalte mit NOW(). +async function processWindow({ flagColumn, daysFrom, daysTo, sender }) { + const [rows] = await pool.query( + `SELECT id, name, kontakt_email, slug, demo_expires_at + FROM tenants + WHERE typ = 'demo' + AND kontakt_email IS NOT NULL + AND ${flagColumn} IS NULL + AND demo_expires_at IS NOT NULL + AND demo_expires_at BETWEEN DATE_ADD(NOW(), INTERVAL ? DAY) + AND DATE_ADD(NOW(), INTERVAL ? DAY)`, + [daysFrom, daysTo] + ); + for (const t of rows) { + try { + await sender(t); + await pool.query(`UPDATE tenants SET ${flagColumn} = NOW() WHERE id = ?`, [t.id]); + console.log(`[expiry] ${flagColumn} -> ${t.kontakt_email} (slug=${t.slug})`); + } catch (e) { + console.error(`[expiry] Mail-Fehler ${flagColumn} fuer ${t.kontakt_email}:`, e.message); + } + } +} + +async function processExpiredToday() { + const [rows] = await pool.query( + `SELECT id, name, kontakt_email, slug, demo_expires_at + FROM tenants + WHERE typ = 'demo' + AND kontakt_email IS NOT NULL + AND notified_expired_at IS NULL + AND demo_expires_at IS NOT NULL + AND demo_expires_at <= NOW()` + ); + for (const t of rows) { + try { + await sendDemoExpiredMail({ to: t.kontakt_email, name: t.name }); + await pool.query( + `UPDATE tenants SET notified_expired_at = NOW(), + status = 'abgelaufen' + WHERE id = ?`, + [t.id] + ); + console.log(`[expiry] expired-mail -> ${t.kontakt_email} (slug=${t.slug})`); + } catch (e) { + console.error(`[expiry] expired-Mail-Fehler ${t.kontakt_email}:`, e.message); + } + } +} + +export async function runExpiryCheck() { + console.log('[expiry] check start', new Date().toISOString()); + // 7-Tage-Erinnerung: Ablauf in [6, 7] Tagen + await processWindow({ + flagColumn: 'notified_7d_at', + daysFrom: 6, daysTo: 7, + sender: (t) => sendDemoExpiryReminderMail({ + to: t.kontakt_email, name: t.name, slug: t.slug, + daysLeft: 7, expiresAt: t.demo_expires_at, + }), + }); + // 1-Tag-Erinnerung: Ablauf in [0, 1] Tagen (aber noch nicht abgelaufen) + await processWindow({ + flagColumn: 'notified_1d_at', + daysFrom: 0, daysTo: 1, + sender: (t) => sendDemoLastDayMail({ + to: t.kontakt_email, name: t.name, slug: t.slug, + expiresAt: t.demo_expires_at, + }), + }); + // Heute (oder bereits) abgelaufen + await processExpiredToday(); + console.log('[expiry] check done'); +} + +export function startExpiryCron() { + // Taeglich um 08:00 Uhr (Server-Zeit = UTC). Anpassbar via env CRON_EXPIRY_AT + const expr = process.env.CRON_EXPIRY_AT || '0 8 * * *'; + cron.schedule(expr, () => { + runExpiryCheck().catch(e => console.error('[expiry] cron error:', e)); + }); + console.log('[expiry] cron registriert:', expr); +} diff --git a/backend/lib/mail.js b/backend/lib/mail.js index e7b0045..5218496 100644 --- a/backend/lib/mail.js +++ b/backend/lib/mail.js @@ -8,11 +8,8 @@ if (process.env.SMTP_HOST && process.env.SMTP_USER && process.env.SMTP_PASS) { transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT || '587'), - secure: (process.env.SMTP_SECURE || 'false') === 'true', // 587 = STARTTLS (secure=false) - auth: { - user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS, - }, + secure: (process.env.SMTP_SECURE || 'false') === 'true', + auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }, }); console.log('[mail] SMTP transport aktiv:', process.env.SMTP_HOST); } @@ -23,20 +20,31 @@ async function sendMail({ to, subject, html }) { if (transporter) { const info = await transporter.sendMail({ from: FROM, to, subject, html }); console.log('[mail] SMTP sent:', to, 'msgId:', info.messageId); - return { provider: 'smtp', messageId: info.messageId, accepted: info.accepted, rejected: info.rejected }; + return { provider: 'smtp', messageId: info.messageId }; } if (resend) { const r = await resend.emails.send({ from: FROM, to, subject, html }); console.log('[mail] Resend sent:', to, 'id:', r.data?.id); - return { provider: 'resend', id: r.data?.id, error: r.error }; + return { provider: 'resend', id: r.data?.id }; } console.log('[mail-dev] (kein Provider konfiguriert) ->', to, subject); return { dev: true }; } +const wrap = (inner) => ` +
+ Dockly-Pfandsystem Demo · pfandsystem.dockly.de +
+Hallo ${name},
dein ${days}-Tage Demo-Zugang ist bereit. Du kannst alle Funktionen @@ -48,20 +56,87 @@ export async function sendDemoInviteMail({ to, name, slug, password, loginUrl, d
Tenant-Kennung: ${slug}
Bitte ändere dein Passwort nach dem ersten Login.
-Diese Mail wurde automatisch versandt vom Dockly-Pfandsystem-Demo-Portal.
- `; - return sendMail({ to, subject: `Dein Dockly-Pfandsystem-Demo-Zugang (${days} Tage)`, html }); + `), + }); } -// Test-Endpoint Helper +// 7 Tage vor Ablauf +export async function sendDemoExpiryReminderMail({ to, name, slug, daysLeft, expiresAt }) { + const loginUrl = `https://pfandsystem.dockly.de/login?tenant=${slug}`; + const dateStr = new Date(expiresAt).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); + return sendMail({ + to, + subject: `Erinnerung: Dein Demo-Zugang läuft in ${daysLeft} Tagen ab`, + html: wrap(` +Hallo ${name},
+nur noch ${daysLeft} Tag${daysLeft === 1 ? '' : 'e'} — danach wird dein + Demo-Zugang am ${dateStr} automatisch deaktiviert.
+Du willst weiter testen oder das System für deinen Betrieb live nehmen? + Antworte einfach auf diese Mail — wir verlängern den Zugang oder + besprechen die Live-Schaltung mit dir.
+ + `), + }); +} + +// 1 Tag vor Ablauf (Letzter-Tag-Variante) +export async function sendDemoLastDayMail({ to, name, slug, expiresAt }) { + const loginUrl = `https://pfandsystem.dockly.de/login?tenant=${slug}`; + const dateStr = new Date(expiresAt).toLocaleDateString('de-DE'); + return sendMail({ + to, + subject: 'Letzter Tag: Dein Demo-Zugang läuft morgen ab', + html: wrap(` +Hallo ${name},
+morgen (${dateStr}) endet dein Demo-Zugang. Falls du noch Daten exportieren oder etwas + abschließend testen möchtest, schau heute nochmal rein.
+Wir würden uns sehr freuen, wenn wir gemeinsam weitermachen. + Eine kurze Antwort auf diese Mail genügt — wir melden uns sofort zurück.
+ + `), + }); +} + +// Demo ist abgelaufen +export async function sendDemoExpiredMail({ to, name }) { + return sendMail({ + to, + subject: 'Dein Demo-Zugang ist abgelaufen', + html: wrap(` +Hallo ${name},
+dein Demo-Zugang zum Dockly-Pfandsystem ist heute abgelaufen. + Deine Daten sind noch 14 Tage bei uns gespeichert — + falls du jetzt verlängern oder live gehen möchtest, antworte einfach auf + diese Mail. Danach werden die Demo-Daten automatisch gelöscht.
+Vielen Dank fürs Testen!
+ `), + }); +} + +// SMTP-Selbsttest (zum Debuggen) export async function sendTestMail(to) { - const html = ` -Diese Test-Mail bestätigt, dass der Mail-Versand über
${process.env.SMTP_HOST || 'kein SMTP'} funktioniert.
Absender: ${FROM}
Zeitstempel: ${new Date().toISOString()}
-