Vertragsverwaltung_Plusfit24/routes/renewal.js
2026-03-27 14:56:59 +00:00

249 lines
9.6 KiB
JavaScript

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 => `
<tr>
<td style="padding:8px 12px;border-bottom:1px solid #eee">${t.name}</td>
<td style="padding:8px 12px;border-bottom:1px solid #eee;text-align:right">
<strong>${Number(t.price_monthly).toFixed(2).replace('.', ',')} €/Monat</strong>
</td>
<td style="padding:8px 12px;border-bottom:1px solid #eee">${t.duration_months} Monate</td>
</tr>`).join('');
return `<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"></head>
<body style="font-family:Outfit,Arial,sans-serif;background:#f8f9ff;margin:0;padding:20px">
<div style="max-width:600px;margin:0 auto;background:white;border-radius:16px;overflow:hidden;box-shadow:0 2px 16px rgba(0,0,0,0.08)">
<div style="background:#2d2dcc;padding:32px;text-align:center">
<h1 style="color:white;margin:0;font-size:1.8rem">Plusfit<span style="color:#a5b4fc">24</span></h1>
<p style="color:#c7d2fe;margin:8px 0 0">Deine Mitgliedschaft läuft bald ab</p>
</div>
<div style="padding:32px">
<p>Hallo ${member.first_name} ${member.last_name},</p>
<p>deine Mitgliedschaft bei PlusFit24 läuft am <strong>${new Date(expiryDate).toLocaleDateString('de-DE')}</strong> aus.</p>
<p>Wir würden uns freuen, dich weiterhin in unserem Studio begrüßen zu dürfen! Wähle jetzt deinen neuen Tarif:</p>
<table style="width:100%;border-collapse:collapse;margin:20px 0;border:1px solid #eee;border-radius:8px;overflow:hidden">
<thead>
<tr style="background:#f0f0ff">
<th style="padding:10px 12px;text-align:left;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px">Tarif</th>
<th style="padding:10px 12px;text-align:right;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px">Preis</th>
<th style="padding:10px 12px;font-size:0.8rem;text-transform:uppercase;letter-spacing:0.5px">Laufzeit</th>
</tr>
</thead>
<tbody>${tarifList}</tbody>
</table>
<div style="text-align:center;margin:28px 0">
<a href="${renewalLink}" style="display:inline-block;background:#2d2dcc;color:white;padding:14px 32px;border-radius:10px;text-decoration:none;font-weight:700;font-size:1rem">
Jetzt Mitgliedschaft verlängern →
</a>
</div>
<p style="color:#6b7280;font-size:0.85rem">
Dieser Link ist 30 Tage gültig. Falls du Fragen hast, melde dich gerne bei uns.<br>
<strong>PlusFit24 UG</strong> · Moosleiten 12 · 84089 Aiglsbach
</p>
</div>
</div>
</body></html>`;
}
// ============================================
// Ö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;