Abo: Widerrufsfrist nach §§ 187, 188, 193 BGB statt 14 x 24 Stunden
Bisher endete die Frist rechnerisch 14 x 24 h nach Vertragsschluss, und der spaetere Beginn lag pauschal 15 Tage danach. Gesetzlich beginnt die Frist am Folgetag und endet um 24 Uhr des 14. Tages; faellt der auf Sa/So/Feiertag, am naechsten Werktag. Beispiel: Abschluss Sa 26.09. -> Frist bis Mo 12.10., 24 Uhr. Vorher verschwand "Vertrag widerrufen" schon Sa 10.10. vormittags, und "Beginn nach Ablauf der Frist" buchte am So 11.10. ab. - server/services/deadlines.js: Fristende und Leistungsbeginn, deutscher Kalender, Feiertage aller Bundeslaender (zugunsten des Kunden, § 193 BGB: Erklaerungsort). - Portal: "Vertrag widerrufen · bis TT.MM." bleibt bis zum echten Fristende sichtbar. - Beginn nach Ablauf der Frist: Stripe trial_end = 6 Uhr am Tag nach dem spaetesten moeglichen Fristende (Checkout gilt hoechstens 24 h). Die Bestellseite zeigt genau dieses Datum; hat es sich um Mitternacht verschoben, kommt "Seite neu laden". - Team-Mail beim Widerruf nennt das Fristende und ob der Widerruf rechtzeitig kam. - Datumsanzeige im Portal nach deutscher Zeit (auch bei Kunden im Ausland). Vertragstexte unveraendert (termsVersion bleibt v3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
93381a81c5
commit
e54f97144c
5 files changed
+144
-27
No files matched your search
+19
-10
@@ -5,6 +5,7 @@ import { Router } from 'express'
|
||||
import { config } from '../config.js'
|
||||
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
|
||||
import { getCustomerByEmail, getDocument, updateDocument } from '../services/appwriteAdmin.js'
|
||||
import { lateStartAt, withdrawalDeadline } from '../services/deadlines.js'
|
||||
import { getLedger } from '../services/hourLedger.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from '../services/mailer.js'
|
||||
import { activateOfferFromCheckout, getOpenOffer, offerText, offerView } from '../services/offers.js'
|
||||
@@ -30,12 +31,6 @@ const fmtDate = (iso) =>
|
||||
const fmtDateTime = (d) =>
|
||||
new Date(d).toLocaleString('de-DE', { timeZone: 'Europe/Berlin', dateStyle: 'medium', timeStyle: 'short' })
|
||||
|
||||
function addDays(iso, days) {
|
||||
const d = new Date(iso)
|
||||
d.setUTCDate(d.getUTCDate() + days)
|
||||
return d
|
||||
}
|
||||
|
||||
function addMonthsIso(iso, months) {
|
||||
const d = new Date(iso)
|
||||
const day = d.getUTCDate()
|
||||
@@ -55,7 +50,8 @@ function minTermEndOf(sub) {
|
||||
function publicSubscription(sub) {
|
||||
if (!sub) return null
|
||||
const active = ACTIVE_STATUSES.has(sub.status)
|
||||
const withdrawalUntil = sub.startedAt ? addDays(sub.startedAt, config.abo.withdrawalDays) : null
|
||||
// Fristende 24 Uhr, bei Sa/So/Feiertag am nächsten Werktag (deadlines.js)
|
||||
const withdrawalUntil = sub.startedAt ? withdrawalDeadline(sub.startedAt).endsAt : null
|
||||
return {
|
||||
status: sub.status,
|
||||
active,
|
||||
@@ -110,6 +106,8 @@ router.get('/', requireSession, async (req, res) => {
|
||||
subscription: publicSubscription(sub),
|
||||
offer: offer ? offerView(offer) : null,
|
||||
ledger,
|
||||
// Leistungsbeginn bei "Nach Ablauf der Widerrufsfrist" für die Bestellseite
|
||||
lateStartAt: lateStartAt().toISOString(),
|
||||
})
|
||||
} catch (err) {
|
||||
return res.status(500).json({ error: err.message || 'Abo konnte nicht geladen werden' })
|
||||
@@ -122,7 +120,7 @@ router.post('/checkout', requireSession, async (req, res) => {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nicht bestellt werden.' })
|
||||
}
|
||||
const { acceptTerms, startChoice, termsVersion, offerId } = req.body || {}
|
||||
const { acceptTerms, startChoice, termsVersion, offerId, lateStartAt: shownLateStart } = req.body || {}
|
||||
if (acceptTerms !== true) {
|
||||
return res.status(400).json({ error: 'Bitte stimmen Sie den Vertragsbedingungen zu, um zu bestellen.' })
|
||||
}
|
||||
@@ -141,6 +139,11 @@ router.post('/checkout', requireSession, async (req, res) => {
|
||||
if (!isStripeConfigured() || !isPilotAllowed(customer, offer)) {
|
||||
return res.status(403).json({ error: 'Die Online-Bestellung ist noch nicht freigeschaltet.' })
|
||||
}
|
||||
// Ebenso genau der Leistungsbeginn, den die Bestellseite gezeigt hat (verschiebt sich um Mitternacht)
|
||||
const serviceStartAt = startChoice === 'nach_frist' ? lateStartAt() : null
|
||||
if (serviceStartAt && shownLateStart !== serviceStartAt.toISOString()) {
|
||||
return res.status(409).json({ error: 'Der Leistungsbeginn hat sich durch den Datumswechsel verschoben. Bitte laden Sie die Seite neu.' })
|
||||
}
|
||||
// Nachweis der Zustimmung (§ 305 Abs. 2, § 357a Abs. 2 BGB): Zeitpunkt, Version, Wahl, IP, Browser
|
||||
const now = new Date().toISOString()
|
||||
const url = await createAboCheckout(
|
||||
@@ -149,6 +152,7 @@ router.post('/checkout', requireSession, async (req, res) => {
|
||||
termsAcceptedAt: now,
|
||||
startChoice,
|
||||
earlyStartConsentAt: startChoice === 'sofort' ? now : '',
|
||||
serviceStartAt,
|
||||
consentIp: req.ip,
|
||||
consentUserAgent: req.get('user-agent') || '',
|
||||
},
|
||||
@@ -501,13 +505,18 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
'Ihr Webklar-Team',
|
||||
].filter((l) => l !== null)).catch((e) => console.error('[abo] Widerrufsbestaetigung fehlgeschlagen:', e.message))
|
||||
|
||||
// Fristende wie im Portal berechnet (§§ 187, 188, 193 BGB), damit das Team nicht selbst zählen muss
|
||||
const deadline = sub?.startedAt ? withdrawalDeadline(sub.startedAt) : null
|
||||
await sendAdminNotice(`WIDERRUF eingegangen: ${d.email}`, [
|
||||
`${d.name} <${d.email}> ${d.customerNumber ? `(Kd.-Nr. ${d.customerNumber})` : ''}`,
|
||||
sub
|
||||
? `Stripe-Abo ${sub.stripeSubscriptionId} (Beginn ${fmtDate(sub.startedAt)}) auf Kündigung gesetzt.`
|
||||
: 'KEIN aktives Abo zu dieser E-Mail gefunden -> manuell zuordnen!',
|
||||
'TODO: Widerrufsfrist prüfen (14 Tage ab Vertragsschluss), Abo in Stripe sofort beenden,',
|
||||
'Wertersatz berechnen (anteilige Tage) und Rest innerhalb 14 Tagen erstatten (Stripe -> Rückerstattung).',
|
||||
deadline
|
||||
? `Widerrufsfrist bis ${fmtDate(deadline.endsAt)}, 24 Uhr – Eingang ${receivedAt <= deadline.endsAt ? 'RECHTZEITIG' : 'NACH Fristende, vor einer Ablehnung bitte prüfen'}.`
|
||||
: 'Widerrufsfrist: 14 Tage ab dem Tag nach Vertragsschluss bis 24 Uhr; fällt der letzte Tag auf Sa/So/Feiertag, am nächsten Werktag.',
|
||||
'TODO: Abo in Stripe sofort beenden, Wertersatz berechnen (anteilige Tage) und Rest innerhalb 14 Tagen',
|
||||
'erstatten (Stripe -> Rückerstattung).',
|
||||
])
|
||||
|
||||
return res.json({ ok: true, receivedAt: receivedAt.toISOString() })
|
||||
|
||||
Reference in new issue
Block a user