From 4ce99b36bc3640938139778759beff8226e0281a Mon Sep 17 00:00:00 2001 From: cay Date: Sat, 28 Mar 2026 07:59:23 +0000 Subject: [PATCH] dfhe --- routes/admin.js | 2 +- routes/api.js | 98 +++++++++++++++++++++++------------ routes/billing.js | 23 +++++--- views/admin/member-detail.ejs | 14 +++++ 4 files changed, 96 insertions(+), 41 deletions(-) diff --git a/routes/admin.js b/routes/admin.js index 4c4b93a..dc2e521 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -205,7 +205,7 @@ router.post('/members/:id/update', requireAdmin, async (req, res) => { salutation, title, first_name, last_name, birth_date, email, phone, street, address_addition, zip, city, bank_name, account_holder, iban, tariff_id, status, - agreed_price, agreed_duration + agreed_price, agreed_duration, start_package_price } = req.body; try { await db.query(` diff --git a/routes/api.js b/routes/api.js index 162416e..5fdda5d 100644 --- a/routes/api.js +++ b/routes/api.js @@ -1,28 +1,28 @@ const express = require('express'); -const crypto = require('crypto'); -const router = express.Router(); -const dns = require('dns').promises; -const db = require('../config/database'); +const crypto = require('crypto'); +const router = express.Router(); +const dns = require('dns').promises; +const db = require('../config/database'); -// Email Validierung via DNS MX-Record Check +// ============================================ +// E-Mail Validierung via DNS MX-Record +// ============================================ async function verifyEmailDomain(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) return { valid: false, reason: 'Ungültiges E-Mail-Format' }; - const domain = email.split('@')[1]; try { const records = await dns.resolveMx(domain); - if (records && records.length > 0) { - return { valid: true }; - } + if (records && records.length > 0) return { valid: true }; return { valid: false, reason: 'Domain hat keine E-Mail-Server (MX-Records fehlen)' }; } catch (err) { return { valid: false, reason: 'E-Mail-Domain konnte nicht verifiziert werden' }; } } - -// Server-seitige IBAN Prüfung (Modulo-97) +// ============================================ +// IBAN Validierung (Modulo-97) +// ============================================ const IBAN_LENGTHS = { AL:28,AD:24,AT:20,AZ:28,BH:22,BE:16,BA:20,BR:29,BG:22,CR:22,HR:21, CY:28,CZ:24,DK:18,DO:28,EE:20,FO:18,FI:18,FR:27,GE:22,DE:22,GI:23, @@ -32,7 +32,7 @@ const IBAN_LENGTHS = { SK:24,SI:19,ES:24,SE:24,CH:21,TN:24,TR:26,AE:23,GB:22,VG:24 }; function validateIBANServer(iban) { - if (!iban) return { valid: true }; // Optional – kein IBAN = ok + if (!iban) return { valid: true }; const clean = iban.replace(/\s/g, '').toUpperCase(); if (clean.length < 5) return { valid: false, error: 'IBAN zu kurz' }; const country = clean.substring(0, 2); @@ -57,7 +57,6 @@ function validateIBANServer(iban) { router.post('/verify-email', async (req, res) => { const { email } = req.body; if (!email) return res.json({ valid: false, reason: 'Keine E-Mail angegeben' }); - const result = await verifyEmailDomain(email); res.json(result); }); @@ -73,13 +72,21 @@ router.post('/submit-membership', async (req, res) => { guardian_consent } = req.body; + // Pflichtfelder prüfen + if (!tariff_id || !first_name || !last_name || !birth_date || !email || !street || !zip || !city) { + return res.json({ success: false, error: 'Bitte alle Pflichtfelder ausfüllen.' }); + } + if (!agb_accepted || !datenschutz_accepted || !data_correct) { + return res.json({ success: false, error: 'Bitte alle Einverständniserklärungen bestätigen.' }); + } + // E-Mail validieren const emailCheck = await verifyEmailDomain(email); if (!emailCheck.valid) { return res.json({ success: false, error: 'E-Mail-Adresse ist nicht erreichbar: ' + emailCheck.reason }); } - // IBAN prüfen (falls angegeben) + // IBAN prüfen if (iban && iban.trim()) { const ibanCheck = validateIBANServer(iban.trim()); if (!ibanCheck.valid) { @@ -87,49 +94,67 @@ router.post('/submit-membership', async (req, res) => { } } - // Pflichtfelder prüfen - if (!tariff_id || !first_name || !last_name || !birth_date || !email || !street || !zip || !city) { - return res.json({ success: false, error: 'Bitte alle Pflichtfelder ausfüllen.' }); - } - - if (!agb_accepted || !datenschutz_accepted || !data_correct) { - return res.json({ success: false, error: 'Bitte alle Einverständniserklärungen bestätigen.' }); - } - // Alter berechnen const birthDateObj = new Date(birth_date); const today = new Date(); let age = today.getFullYear() - birthDateObj.getFullYear(); - const m = today.getMonth() - birthDateObj.getMonth(); - if (m < 0 || (m === 0 && today.getDate() < birthDateObj.getDate())) age--; + const mo = today.getMonth() - birthDateObj.getMonth(); + if (mo < 0 || (mo === 0 && today.getDate() < birthDateObj.getDate())) age--; const is_minor = age < 18 ? 1 : 0; if (is_minor && !guardian_consent) { return res.json({ success: false, error: 'Bei Minderjährigen ist die Einverständniserklärung der Erziehungsberechtigten erforderlich.' }); } - if (age < 14) { return res.json({ success: false, error: 'Das Mindestalter für eine Mitgliedschaft beträgt 14 Jahre.' }); } - // Tarif prüfen + // Tarif laden — MUSS vor Berechnungen stehen const [tariffs] = await db.query('SELECT * FROM tariffs WHERE id = ? AND active = 1', [tariff_id]); if (tariffs.length === 0) { return res.json({ success: false, error: 'Ungültiger oder inaktiver Tarif.' }); } + const tariff = tariffs[0]; + + // Startpaket-Preis + const startPackagePrice = parseFloat(tariff.start_package_price) || 35.00; + + // Vertragsbeginn = heute + 15 Tage + const contractStart = new Date(); + contractStart.setDate(contractStart.getDate() + 15); + + // Anteiligen ersten Monatsbeitrag berechnen + const daysInMonth = new Date(contractStart.getFullYear(), contractStart.getMonth() + 1, 0).getDate(); + const remainingDays = daysInMonth - contractStart.getDate() + 1; + const dailyRate = parseFloat(tariff.price_monthly) / daysInMonth; + const partialMonth = Math.round(dailyRate * remainingDays * 100) / 100; + + // Erste Zahlung = anteiliger Monat + Startpaket + const firstPaymentAmt = Math.round((partialMonth + startPackagePrice) * 100) / 100; + + // Vertragsende + const contractEnd = new Date(contractStart); + contractEnd.setMonth(contractEnd.getMonth() + tariff.duration_months); + contractEnd.setDate(contractEnd.getDate() - 1); // Zugangstoken generieren const access_token = crypto.randomBytes(16).toString('hex'); // In DB speichern + // Spalten: 26, Werte: 26 await db.query(` INSERT INTO memberships - (tariff_id, salutation, title, first_name, last_name, birth_date, email, phone, - street, address_addition, zip, city, bank_name, account_holder, iban, - sepa_accepted, agb_accepted, datenschutz_accepted, data_correct, guardian_consent, is_minor, access_token, - agreed_price, agreed_duration) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + (tariff_id, salutation, title, first_name, last_name, birth_date, + email, phone, street, address_addition, zip, city, + bank_name, account_holder, iban, + sepa_accepted, agb_accepted, datenschutz_accepted, data_correct, + guardian_consent, is_minor, access_token, + agreed_price, agreed_duration, + start_package_price, + signup_date, contract_start, contract_end, effective_end, + first_payment_date, first_payment_amt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ tariff_id, salutation, title || '', first_name, last_name, birth_date, email, phone || '', street, address_addition || '', zip, city, @@ -137,7 +162,14 @@ router.post('/submit-membership', async (req, res) => { sepa_accepted ? 1 : 0, agb_accepted ? 1 : 0, datenschutz_accepted ? 1 : 0, data_correct ? 1 : 0, guardian_consent ? 1 : 0, is_minor, access_token, - tariffs[0].price_monthly, tariffs[0].duration_months + tariff.price_monthly, tariff.duration_months, + startPackagePrice, + today.toISOString().split('T')[0], + contractStart.toISOString().split('T')[0], + contractEnd.toISOString().split('T')[0], + contractEnd.toISOString().split('T')[0], + contractStart.toISOString().split('T')[0], + firstPaymentAmt ]); res.json({ success: true }); diff --git a/routes/billing.js b/routes/billing.js index 9df71a6..7ec60cb 100644 --- a/routes/billing.js +++ b/routes/billing.js @@ -13,16 +13,23 @@ function calcInvoiceAmount(member, period) { // Pausiert → 0€ if (member.status === 'paused') return 0; - // Vereinbarten Preis verwenden (nicht den aktuellen Tarif-Preis!) const price = parseFloat(member.agreed_price || member.price_monthly); - // Erster Monat (anteilig)? const firstPeriod = member.first_payment_date - ? member.first_payment_date.toISOString().substring(0, 7) + ? new Date(member.first_payment_date).toISOString().substring(0, 7) : null; - if (firstPeriod === period && member.first_payment_amt) { - return parseFloat(member.first_payment_amt); + if (firstPeriod === period) { + // Startpaket nur wenn nicht erlassen (start_package_price > 0) + const startPkg = parseFloat(member.start_package_price || 0); + const daysInMonth = new Date( + new Date(member.first_payment_date).getFullYear(), + new Date(member.first_payment_date).getMonth() + 1, 0 + ).getDate(); + const day = new Date(member.first_payment_date).getDate(); + const remaining = daysInMonth - day + 1; + const partial = Math.round((price / daysInMonth) * remaining * 100) / 100; + return Math.round((partial + startPkg) * 100) / 100; } return price; @@ -78,7 +85,8 @@ router.get('/', requireAdmin, async (req, res) => { // Vorschau: Mitglieder die noch keine Rechnung für diese Periode haben const [eligible] = await db.query(` SELECT m.*, t.price_monthly, t.name as tariff_name, - COALESCE(m.agreed_price, t.price_monthly) as agreed_price + COALESCE(m.agreed_price, t.price_monthly) as agreed_price, + COALESCE(m.start_package_price, 0) as start_package_price FROM memberships m JOIN tariffs t ON m.tariff_id = t.id WHERE m.status IN ('active','paused') @@ -124,7 +132,8 @@ router.post('/run', requireAdmin, async (req, res) => { // Alle aktiven/pausierten Mitglieder im Vertragszeitraum const [members] = await db.query(` SELECT m.*, t.price_monthly, t.name as tariff_name, - COALESCE(m.agreed_price, t.price_monthly) as agreed_price + COALESCE(m.agreed_price, t.price_monthly) as agreed_price, + COALESCE(m.start_package_price, 0) as start_package_price FROM memberships m JOIN tariffs t ON m.tariff_id = t.id WHERE m.status IN ('active','paused') diff --git a/views/admin/member-detail.ejs b/views/admin/member-detail.ejs index 04dcebd..dfe53f0 100644 --- a/views/admin/member-detail.ejs +++ b/views/admin/member-detail.ejs @@ -165,6 +165,20 @@ +
+
+ + +
+
+ + +
+