diff --git a/app.js b/app.js index c7c0d40..9cfb8fc 100644 --- a/app.js +++ b/app.js @@ -35,6 +35,7 @@ const adminRouter = require('./routes/admin'); const apiRouter = require('./routes/api'); const billingRouter = require('./routes/billing'); const financeRouter = require('./routes/finance'); +const renewalRouter = require('./routes/renewal'); const cron = require('node-cron'); app.use('/', indexRouter); @@ -42,6 +43,7 @@ app.use('/admin', adminRouter); app.use('/api', apiRouter); app.use('/admin/billing', billingRouter); app.use('/admin/finance', financeRouter); +app.use('/', renewalRouter); // 404 Handler app.use((req, res) => { @@ -71,6 +73,58 @@ async function initAdmin() { } } +// Verlängerungs-E-Mails: täglich um 08:00 Uhr prüfen (60 Tage vorher) +cron.schedule('0 8 * * *', async () => { + console.log('⏰ Prüfe auslaufende Verträge (60 Tage)...'); + try { + const [expiring] = await db.query(` + SELECT m.* FROM memberships m + WHERE m.status = 'active' + AND m.effective_end BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 61 DAY) + AND m.id NOT IN ( + SELECT membership_id FROM renewal_requests + WHERE status IN ('pending','completed') + AND sent_at >= DATE_SUB(NOW(), INTERVAL 70 DAY) + ) + `); + if (expiring.length > 0) { + const { renewalEmailHtml } = require('./routes/renewal'); + const mailer = require('./config/mailer'); + const crypto = require('crypto'); + const [tariffs] = await db.query('SELECT * FROM tariffs WHERE active = 1 ORDER BY price_monthly ASC'); + for (const member of expiring) { + try { + const token = crypto.randomBytes(32).toString('hex'); + const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); + await db.query( + 'INSERT INTO renewal_requests (membership_id, token, expires_at) VALUES (?,?,?)', + [member.id, token, expiresAt] + ); + const baseUrl = process.env.APP_URL || 'https://plusfit24.software-joksch.com'; + const link = baseUrl + '/renew/' + token; + await mailer.sendMail({ + from: process.env.MAIL_FROM, + to: member.email, + subject: `Deine PlusFit24 Mitgliedschaft läuft bald ab`, + html: renewalEmailHtml(member, tariffs, link, member.effective_end || member.contract_end) + }); + await db.query( + 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)', + [member.id, 'renewal_auto', member.email, 'Mitgliedschaft läuft ab', 'sent'] + ); + console.log('📧 Verlängerungs-E-Mail gesendet an:', member.email); + } catch (err) { + console.error('❌ E-Mail Fehler für', member.email, ':', err.message); + } + } + } else { + console.log('✅ Keine auslaufenden Verträge heute.'); + } + } catch (err) { + console.error('❌ Cron Fehler:', err.message); + } +}); + // Auto-Abrechnungslauf jeden 1. des Monats um 06:00 Uhr cron.schedule('0 6 1 * *', async () => { const { currentPeriod } = require('./routes/billing'); diff --git a/config/mailer.js b/config/mailer.js new file mode 100644 index 0000000..c80e0c9 --- /dev/null +++ b/config/mailer.js @@ -0,0 +1,20 @@ +const nodemailer = require('nodemailer'); + +const transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST || 'smtp.ionos.de', + port: parseInt(process.env.MAIL_PORT) || 587, + secure: process.env.MAIL_SECURE === 'true', + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASSWORD + }, + tls: { rejectUnauthorized: false } +}); + +// Verbindung testen beim Start +transporter.verify((err) => { + if (err) console.error('❌ E-Mail Verbindung fehlgeschlagen:', err.message); + else console.log('✅ E-Mail Server verbunden:', process.env.MAIL_HOST); +}); + +module.exports = transporter; diff --git a/package.json b/package.json index b41b51d..7a8b083 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "dns": "^0.2.2", "pdfkit": "^0.14.0", "node-cron": "^3.0.3", - "multer": "^1.4.5-lts.1" + "multer": "^1.4.5-lts.1", + "nodemailer": "^6.9.7" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/routes/renewal.js b/routes/renewal.js new file mode 100644 index 0000000..a3e3f36 --- /dev/null +++ b/routes/renewal.js @@ -0,0 +1,248 @@ +const express = require('express'); +const router = express.Router(); +const crypto = require('crypto'); +const db = require('../config/database'); +const mailer = require('../config/mailer'); +const { requireAdmin } = require('../middleware/auth'); + +// ============================================ +// E-Mail Templates +// ============================================ +function renewalEmailHtml(member, tariffs, renewalLink, expiryDate) { + const tarifList = tariffs.map(t => ` + + ${t.name} + + ${Number(t.price_monthly).toFixed(2).replace('.', ',')} €/Monat + + ${t.duration_months} Monate + `).join(''); + + return ` + + + +
+
+

Plusfit24

+

Deine Mitgliedschaft läuft bald ab

+
+
+

Hallo ${member.first_name} ${member.last_name},

+

deine Mitgliedschaft bei PlusFit24 läuft am ${new Date(expiryDate).toLocaleDateString('de-DE')} aus.

+

Wir würden uns freuen, dich weiterhin in unserem Studio begrüßen zu dürfen! Wähle jetzt deinen neuen Tarif:

+ + + + + + + + + + ${tarifList} +
TarifPreisLaufzeit
+ +
+ + Jetzt Mitgliedschaft verlängern → + +
+ +

+ Dieser Link ist 30 Tage gültig. Falls du Fragen hast, melde dich gerne bei uns.
+ PlusFit24 UG · Moosleiten 12 · 84089 Aiglsbach +

+
+
+`; +} + +// ============================================ +// Öffentliche Verlängerungsseite (für Mitglieder) +// ============================================ +router.get('/renew/:token', async (req, res) => { + try { + const [rows] = await db.query(` + SELECT r.*, m.first_name, m.last_name, m.email, + m.agreed_price, m.tariff_id + FROM renewal_requests r + JOIN memberships m ON r.membership_id = m.id + WHERE r.token = ? AND r.status = 'pending' AND r.expires_at > NOW() + `, [req.params.token]); + + if (rows.length === 0) { + return res.render('renewal-expired'); + } + + const [tariffs] = await db.query( + 'SELECT * FROM tariffs WHERE active = 1 ORDER BY price_monthly ASC' + ); + + res.render('renewal', { + request: rows[0], + tariffs, + error: null, + success: null + }); + } catch (err) { + console.error(err); + res.render('error', { message: 'Fehler beim Laden der Seite.' }); + } +}); + +router.post('/renew/:token', async (req, res) => { + const { tariff_id } = req.body; + try { + const [rows] = await db.query(` + SELECT r.*, m.first_name, m.last_name, m.email, + m.contract_end, m.effective_end + FROM renewal_requests r + JOIN memberships m ON r.membership_id = m.id + WHERE r.token = ? AND r.status = 'pending' AND r.expires_at > NOW() + `, [req.params.token]); + + if (rows.length === 0) return res.render('renewal-expired'); + const request = rows[0]; + + const [tariffs] = await db.query('SELECT * FROM tariffs WHERE id = ? AND active = 1', [tariff_id]); + if (tariffs.length === 0) { + return res.render('renewal', { request, tariffs: [], error: 'Ungültiger Tarif.', success: null }); + } + const tariff = tariffs[0]; + + // Neues Vertragsende berechnen + const startDate = new Date(request.effective_end || request.contract_end || new Date()); + const newEnd = new Date(startDate); + newEnd.setMonth(newEnd.getMonth() + tariff.duration_months); + + await db.query(` + UPDATE memberships SET + tariff_id = ?, + agreed_price = ?, + agreed_duration = ?, + contract_end = ?, + effective_end = ?, + status = 'active' + WHERE id = ? + `, [ + tariff.id, tariff.price_monthly, tariff.duration_months, + newEnd, newEnd, request.membership_id + ]); + + await db.query( + "UPDATE renewal_requests SET status='completed', completed_at=NOW() WHERE id=?", + [request.id] + ); + + res.render('renewal', { + request, tariffs: [tariff], + error: null, + success: `Deine Mitgliedschaft wurde erfolgreich verlängert! Neuer Tarif: ${tariff.name} bis ${newEnd.toLocaleDateString('de-DE')}` + }); + } catch (err) { + console.error(err); + res.render('error', { message: 'Fehler bei der Verlängerung.' }); + } +}); + +// ============================================ +// Admin: Verlängerungs-E-Mail manuell senden +// ============================================ +router.post('/admin/send-renewal/:memberId', requireAdmin, async (req, res) => { + const memberId = req.params.memberId; + const backUrl = req.headers.referer || '/admin/finance'; + try { + const [members] = await db.query(` + SELECT m.*, t.name as tariff_name + FROM memberships m + LEFT JOIN tariffs t ON m.tariff_id = t.id + WHERE m.id = ? + `, [memberId]); + + if (members.length === 0) return res.redirect(backUrl + '?error=Mitglied+nicht+gefunden'); + const member = members[0]; + + // Alten Pending-Request invalidieren + await db.query( + "UPDATE renewal_requests SET status='expired' WHERE membership_id=? AND status='pending'", + [memberId] + ); + + // Neuen Token generieren + const token = crypto.randomBytes(32).toString('hex'); + const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 Tage + + await db.query( + 'INSERT INTO renewal_requests (membership_id, token, expires_at) VALUES (?, ?, ?)', + [memberId, token, expiresAt] + ); + + // Aktive Tarife laden + const [tariffs] = await db.query('SELECT * FROM tariffs WHERE active = 1 ORDER BY price_monthly ASC'); + const baseUrl = `${req.protocol}://${req.get('host')}`; + const renewalLink = `${baseUrl}/renew/${token}`; + const expiryDate = member.effective_end || member.contract_end; + + // E-Mail senden + await mailer.sendMail({ + from: process.env.MAIL_FROM, + to: member.email, + subject: `Deine PlusFit24 Mitgliedschaft läuft am ${new Date(expiryDate).toLocaleDateString('de-DE')} ab`, + html: renewalEmailHtml(member, tariffs, renewalLink, expiryDate) + }); + + // Log + await db.query( + 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)', + [memberId, 'renewal', member.email, 'Mitgliedschaft läuft ab', 'sent'] + ); + + res.redirect(backUrl + '?success=Verlängerungs-E-Mail+an+' + encodeURIComponent(member.email) + '+gesendet'); + } catch (err) { + console.error(err); + // Log failure + await db.query( + 'INSERT INTO email_log (membership_id, type, recipient, subject, status) VALUES (?,?,?,?,?)', + [memberId, 'renewal', '', 'Verlängerung', 'failed'] + ).catch(() => {}); + res.redirect(backUrl + '?error=E-Mail+Fehler:+' + encodeURIComponent(err.message)); + } +}); + +// Admin: Verlängerung manuell durchführen +router.post('/admin/renew-manual/:memberId', requireAdmin, async (req, res) => { + const { new_tariff_id } = req.body; + const memberId = req.params.memberId; + try { + const [tariffs] = await db.query('SELECT * FROM tariffs WHERE id = ?', [new_tariff_id]); + if (tariffs.length === 0) return res.redirect(`/admin/members/${memberId}?error=Tarif+nicht+gefunden`); + const tariff = tariffs[0]; + + const [members] = await db.query('SELECT * FROM memberships WHERE id = ?', [memberId]); + const member = members[0]; + + const startDate = new Date(member.effective_end || member.contract_end || new Date()); + const newEnd = new Date(startDate); + newEnd.setMonth(newEnd.getMonth() + tariff.duration_months); + + await db.query(` + UPDATE memberships SET + tariff_id = ?, + agreed_price = ?, + agreed_duration = ?, + contract_end = ?, + effective_end = ?, + status = 'active' + WHERE id = ? + `, [tariff.id, tariff.price_monthly, tariff.duration_months, newEnd, newEnd, memberId]); + + res.redirect(`/admin/members/${memberId}?success=Vertrag+verlängert+bis+${newEnd.toLocaleDateString('de-DE')}`); + } catch (err) { + console.error(err); + res.redirect(`/admin/members/${memberId}?error=Fehler+bei+Verlängerung`); + } +}); + +module.exports = router; +module.exports.renewalEmailHtml = renewalEmailHtml; diff --git a/views/admin/finance.ejs b/views/admin/finance.ejs index c58233d..cef21b4 100644 --- a/views/admin/finance.ejs +++ b/views/admin/finance.ejs @@ -275,7 +275,12 @@ <%= Number(m.price_monthly).toFixed(2).replace('.', ',') %> € - Zur Karteikarte +
+ Karteikarte +
+ +
+
<% }) %> diff --git a/views/admin/member-detail.ejs b/views/admin/member-detail.ejs index e82e430..5e4772e 100644 --- a/views/admin/member-detail.ejs +++ b/views/admin/member-detail.ejs @@ -455,6 +455,41 @@ + + +
+
+ 🔄 +

Vertragsverlängerung

+
+
+
+ +
+
+ + +
+ +
+ +
+ +
+
+
+
+ diff --git a/views/renewal-expired.ejs b/views/renewal-expired.ejs new file mode 100644 index 0000000..4d456ec --- /dev/null +++ b/views/renewal-expired.ejs @@ -0,0 +1,18 @@ + + +PlusFit24 – Link abgelaufen + + + + + +
+
+
+

Link abgelaufen

+

Dieser Verlängerungslink ist nicht mehr gültig.

+

Bitte kontaktiere uns direkt im Studio oder ruf uns an.

+ Zur Startseite +
+
+ diff --git a/views/renewal.ejs b/views/renewal.ejs new file mode 100644 index 0000000..991ea9f --- /dev/null +++ b/views/renewal.ejs @@ -0,0 +1,70 @@ + + + + + + PlusFit24 – Mitgliedschaft verlängern + + + + + +
+
+

Mitgliedschaft verlängern

+

Hallo <%= request.first_name %>! Wähle deinen neuen Tarif.

+ + <% if (success) { %> +
<%= success %>
+ Zurück zur Startseite + <% } else { %> + <% if (error) { %>
<%= error %>
<% } %> + +
+
+ <% tariffs.forEach(tariff => { %> + + <% }) %> +
+ +
+ <% } %> +
+
+ + + +