Wartungs-Abo v3: Mindestlaufzeit 12 Monate
- Bedingungen § 8: 12 Monate ab Leistungsbeginn, Kuendigung zum Ende bis 1 Monat vorher, danach monatlich; Sonderkuendigung bei Geschaeftsaufgabe (1 Monat Frist), einvernehmliche Beendigung jederzeit; gilt beidseitig - Kuendigungslogik computeCancellationDate (Mindestlaufzeit, verpasste Frist, Wunschtermin, Geschaeftsaufgabe, Monatsende) + cancelAt gespiegelt - Stripe-Kundenportal: Kuendigung aus (Stripe kennt keine Mindestlaufzeit) - Bestellseite, Abo-Karte, Vertragsbestaetigung, Checkout-Text, PDF, Leitfaden angepasst; termsVersion 2026-09-25-v3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e21aa0690
commit
9b563482ff
11 files changed
+112
-56
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -45,7 +45,7 @@
|
||||
|
||||
<div class="box">
|
||||
<strong>Das Wartungs-Abo auf einen Blick</strong><br>
|
||||
50 € pro Monat, ohne Mindestlaufzeit, monatlich kündbar. Enthalten sind Betrieb und Pflege der Website,
|
||||
50 € pro Monat, Mindestlaufzeit 12 Monate, danach monatlich kündbar. Enthalten sind Betrieb und Pflege der Website,
|
||||
1 Domain, 1 E-Mail-Postfach und 1 Stunde Arbeitszeit pro Monat (sammelt sich an, jede Stunde 12 Monate gültig).
|
||||
Alle Details stehen in den Abo-Bedingungen: project.webklar.com/abo-bedingungen.html
|
||||
</div>
|
||||
|
||||
Binary file not shown.
@@ -36,7 +36,7 @@
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar Wartungs-Abo · Vertragsunterlagen"; font: 8pt sans-serif; color: #888; }
|
||||
@top-right { content: "Version 2 · Stand 25.09.2026"; font: 8pt sans-serif; color: #888; }
|
||||
@top-right { content: "Version 3 · Stand 25.09.2026"; font: 8pt sans-serif; color: #888; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #888; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Wartungs-Abo</h1>
|
||||
<p class="meta">Vertragsbedingungen mit Leistungsbeschreibung, Widerrufsbelehrung und Auftragsverarbeitungsvertrag · Version 2 · Stand 25.09.2026</p>
|
||||
<p class="meta">Vertragsbedingungen mit Leistungsbeschreibung, Widerrufsbelehrung und Auftragsverarbeitungsvertrag · Version 3 · Stand 25.09.2026</p>
|
||||
<p class="screen-only meta"><a href="/WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf">Als PDF herunterladen und ausdrucken</a></p>
|
||||
|
||||
<div class="box">
|
||||
@@ -69,8 +69,8 @@
|
||||
<tr><td>Enthalten</td><td>Betrieb und Pflege Ihrer Website bzw. Web-Anwendung · 1 Domain · 1 E-Mail-Postfach · 1 Stunde Arbeitszeit pro Monat</td></tr>
|
||||
<tr><td>Stundenkontingent</td><td>1 Stunde je bezahltem Monat; nicht genutzte Zeit bleibt 12 Monate verfügbar. Mehraufwand nur nach Ihrer Freigabe, 100 € pro Stunde.</td></tr>
|
||||
<tr><td>Zahlung</td><td>Kreditkarte oder SEPA-Lastschrift über Stripe</td></tr>
|
||||
<tr><td>Laufzeit</td><td>unbestimmt, keine Mindestlaufzeit, jederzeit zum Ende des laufenden Abrechnungsmonats kündbar</td></tr>
|
||||
<tr><td>Kündigen</td><td>project.webklar.com/kuendigen.html (ohne Anmeldung), im Kundenportal oder per E-Mail</td></tr>
|
||||
<tr><td>Laufzeit</td><td>Mindestlaufzeit 12 Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach unbefristet und jederzeit zum Ende des Abrechnungsmonats kündbar. Bei Geschäftsaufgabe Sonderkündigung mit einem Monat Frist.</td></tr>
|
||||
<tr><td>Kündigen</td><td>project.webklar.com/kuendigen.html (ohne Anmeldung) oder per E-Mail</td></tr>
|
||||
<tr><td>Widerruf</td><td>Verbraucher können innerhalb von 14 Tagen widerrufen: project.webklar.com/widerrufen.html (siehe Widerrufsbelehrung)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -210,11 +210,13 @@
|
||||
|
||||
<h2 id="p8">§ 8 Laufzeit und Kündigung</h2>
|
||||
<ol class="abs">
|
||||
<li>Der Vertrag läuft auf unbestimmte Zeit. Eine Mindestlaufzeit gibt es nicht.</li>
|
||||
<li>Der Kunde kann jederzeit zum Ende des laufenden Abrechnungsmonats kündigen. Bis dahin bleiben alle Leistungen verfügbar. Die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
|
||||
<li>Wir können mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen.</li>
|
||||
<li>Der Kunde kann über die Schaltfläche „Verträge hier kündigen“ unter project.webklar.com/kuendigen.html kündigen (ohne Anmeldung), im Kundenportal unter „Abo verwalten“ oder in Textform, etwa per E-Mail.</li>
|
||||
<li>Der Vertrag hat eine Mindestlaufzeit von zwölf Monaten ab Leistungsbeginn, bei sofortigem Beginn also ab Vertragsschluss, sonst ab Ablauf der Widerrufsfrist.</li>
|
||||
<li>Beide Seiten können zum Ende der Mindestlaufzeit mit einer Frist von einem Monat kündigen. Wird nicht fristgerecht gekündigt, läuft der Vertrag auf unbestimmte Zeit weiter.</li>
|
||||
<li>Nach Ablauf der Mindestlaufzeit kann der Kunde jederzeit zum Ende des laufenden Abrechnungsmonats kündigen. Wir können dann mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen.</li>
|
||||
<li>Gibt der Kunde den Geschäftsbetrieb, für den die Website bestimmt ist, dauerhaft auf, kann er auch während der Mindestlaufzeit mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen. Auf Verlangen weist er die Geschäftsaufgabe nach, etwa durch die Gewerbeabmeldung.</li>
|
||||
<li>Eine vorzeitige Beendigung im gegenseitigen Einvernehmen ist jederzeit möglich.</li>
|
||||
<li>Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt.</li>
|
||||
<li>Der Kunde kann über die Schaltfläche „Verträge hier kündigen“ unter project.webklar.com/kuendigen.html kündigen (ohne Anmeldung) oder in Textform, etwa per E-Mail. Den Zeitpunkt, zu dem der Vertrag endet, bestätigen wir unverzüglich per E-Mail. Bis dahin bleiben alle Leistungen verfügbar. Die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p9">§ 9 Vertragsende</h2>
|
||||
|
||||
+6
-5
@@ -613,13 +613,13 @@ function renderAboOffer(card, data, { ended } = {}) {
|
||||
<p class="abo-kicker">${ended ? 'Ihr Abo ist beendet' : 'Rundum versorgt'}</p>
|
||||
<h2 class="abo-title">Wartungs-Abo</h2>
|
||||
</div>
|
||||
<div class="abo-price"><strong>50 €</strong><span>pro Monat · monatlich kündbar</span></div>
|
||||
<div class="abo-price"><strong>50 €</strong><span>pro Monat · 12 Monate Mindestlaufzeit</span></div>
|
||||
</div>
|
||||
${aboFeaturesHtml()}
|
||||
<div class="abo-actions">
|
||||
<button type="button" class="abo-btn" id="abo-start" ${available ? '' : 'disabled'}>Abo abschließen</button>
|
||||
<span class="abo-note">${available
|
||||
? 'Keine Mindestlaufzeit. Zahlung per Kreditkarte oder SEPA-Lastschrift. Gemäß § 19 UStG ohne Umsatzsteuer.'
|
||||
? 'Mindestlaufzeit 12 Monate, danach monatlich kündbar. Zahlung per Kreditkarte oder SEPA-Lastschrift. Gemäß § 19 UStG ohne Umsatzsteuer.'
|
||||
: 'Die Online-Bestellung wird gerade eingerichtet – schreiben Sie uns gern im Chat.'}</span>
|
||||
</div>
|
||||
<div class="abo-order hidden" id="abo-order">
|
||||
@@ -642,7 +642,7 @@ function renderAboOffer(card, data, { ended } = {}) {
|
||||
<tr><td>Leistung</td><td>WEBklar Wartungs-Abo: Betrieb & Pflege Ihrer Website bzw. Web-Anwendung, 1 Domain, 1 E-Mail-Postfach (5 GB), 1 Stunde Arbeitszeit je bezahltem Monat (12 Monate nutzbar)</td></tr>
|
||||
<tr><td>Preis</td><td><strong>50,00 € pro Monat</strong> (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), keine weiteren Kosten</td></tr>
|
||||
<tr><td>Abrechnung</td><td>monatlich im Voraus, erstmals zum Leistungsbeginn</td></tr>
|
||||
<tr><td>Laufzeit</td><td>unbestimmt, keine Mindestlaufzeit, jederzeit zum Ende des Abrechnungsmonats kündbar</td></tr>
|
||||
<tr><td>Laufzeit</td><td><strong>Mindestlaufzeit 12 Monate</strong> ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach unbefristet und jederzeit zum Ende des Abrechnungsmonats kündbar</td></tr>
|
||||
<tr><td>Zahlung</td><td>Kreditkarte oder SEPA-Lastschrift über Stripe</td></tr>
|
||||
<tr><td>Anbieter</td><td>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler · Telefon 0176 23726355 · kenso.gri@gmail.com</td></tr>
|
||||
</table>
|
||||
@@ -700,7 +700,7 @@ function renderAboActive(card, data) {
|
||||
if (sub.serviceStartsAt) {
|
||||
statusLine = `<span class="pill pill-muted">beginnt am ${fmtDateShort(sub.serviceStartsAt)}</span>`
|
||||
} else if (sub.cancelAtPeriodEnd) {
|
||||
statusLine = `<span class="pill pill-muted">gekündigt zum ${fmtDateShort(sub.currentPeriodEnd)}</span>`
|
||||
statusLine = `<span class="pill pill-muted">gekündigt zum ${fmtDateShort(sub.cancelAt || sub.currentPeriodEnd)}</span>`
|
||||
} else if (sub.paymentProblem) {
|
||||
statusLine = '<span class="pill pill-red">Zahlung offen</span>'
|
||||
} else {
|
||||
@@ -731,11 +731,12 @@ function renderAboActive(card, data) {
|
||||
${l.nextExpiry ? `<div class="abo-stat"><strong>${fmtHours(l.nextExpiry.minutes)}</strong><span>verfallen am ${fmtDateShort(l.nextExpiry.at)}</span></div>` : ''}
|
||||
</div>
|
||||
<p class="abo-note">Jeden bezahlten Monat kommt 1 Stunde dazu. Angerechnet wird in 15-Minuten-Schritten, die ältesten Stunden zuerst.
|
||||
Aufträge schicken Sie uns einfach über den Chat.</p>
|
||||
Aufträge schicken Sie uns einfach über den Chat.${sub.minTermEnd && !sub.cancelAtPeriodEnd ? ` Mindestlaufzeit bis ${fmtDateShort(sub.minTermEnd)}, danach monatlich kündbar.` : ''}</p>
|
||||
<div class="abo-actions" style="margin-top:14px">
|
||||
<button type="button" class="portal-btn-outline" id="abo-manage">${sub.paymentProblem ? 'Zahlungsart aktualisieren' : 'Abo verwalten · Rechnungen'}</button>
|
||||
${sub.withdrawalOpen ? `<a class="abo-withdraw" href="/widerrufen.html">Vertrag widerrufen</a><span class="abo-note">möglich bis ${fmtDateShort(sub.withdrawalUntil)}</span>` : ''}
|
||||
<a class="abo-note" href="/abo-bedingungen.html" target="_blank" rel="noopener">Leistungen & Bedingungen</a>
|
||||
${sub.cancelAtPeriodEnd ? '' : '<a class="abo-note" href="/kuendigen.html">Kündigen</a>'}
|
||||
</div>
|
||||
${usageRows || creditRows ? `
|
||||
<details class="abo-history">
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<legend data-label-for="kind" data-label="Art der Kündigung">Art der Kündigung</legend>
|
||||
<label class="radio"><input type="radio" name="kind" value="ordentlich" data-text="Ordentliche Kündigung" checked> Ordentliche Kündigung</label>
|
||||
<label class="radio"><input type="radio" name="kind" value="ausserordentlich" data-text="Außerordentliche Kündigung"> Außerordentliche Kündigung aus wichtigem Grund</label>
|
||||
<label class="radio"><input type="radio" name="kind" value="geschaeftsaufgabe" data-text="Sonderkündigung wegen Geschäftsaufgabe"> Sonderkündigung wegen Geschäftsaufgabe</label>
|
||||
</fieldset>
|
||||
<div id="reason-wrap" class="hidden">
|
||||
<label for="reason" data-label-for="reason" data-label="Kündigungsgrund">Kündigungsgrund</label>
|
||||
|
||||
@@ -109,6 +109,8 @@ await ensureCollection(
|
||||
{ type: 'string', key: 'consentIp', size: 64 },
|
||||
{ type: 'string', key: 'consentUserAgent', size: 300 },
|
||||
{ type: 'string', key: 'consentTextVersion', size: 32 },
|
||||
// v3: Beendigungszeitpunkt (Mindestlaufzeit / Wunschtermin)
|
||||
{ type: 'datetime', key: 'cancelAt' },
|
||||
],
|
||||
[
|
||||
{ key: 'idx_customer', type: 'key', attributes: ['customerId'] },
|
||||
|
||||
+4
-2
@@ -61,7 +61,7 @@ export const config = {
|
||||
priceAmountCents: 5000,
|
||||
productName: 'WEBklar Wartungs-Abo',
|
||||
productDescription:
|
||||
'Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland (Sicherheitsupdates, SSL, tägliche Datensicherung, Überwachung der Erreichbarkeit), 1 Domain inklusive Verwaltung, 1 E-Mail-Postfach (5 GB) und 1 Stunde Arbeitszeit pro Monat (12 Monate nutzbar). Keine Mindestlaufzeit, monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.',
|
||||
'Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland (Sicherheitsupdates, SSL, tägliche Datensicherung, Überwachung der Erreichbarkeit), 1 Domain inklusive Verwaltung, 1 E-Mail-Postfach (5 GB) und 1 Stunde Arbeitszeit pro Monat (12 Monate nutzbar). Mindestlaufzeit 12 Monate, danach monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.',
|
||||
statementDescriptor: 'WEBKLAR WARTUNG',
|
||||
productImage: 'https://project.webklar.com/img/wartungs-abo-produkt.png',
|
||||
// Kleinunternehmer: Pflichthinweis auf jeder Stripe-Rechnung
|
||||
@@ -72,7 +72,9 @@ export const config = {
|
||||
|
||||
abo: {
|
||||
// Bei jeder Textaenderung an Bedingungen oder Bestellseite hochziehen (wird mit der Zustimmung gespeichert)
|
||||
termsVersion: '2026-09-25-v2',
|
||||
termsVersion: '2026-09-25-v3',
|
||||
// Mindestlaufzeit ab Leistungsbeginn; Kuendigung zum Ende bis 1 Monat vorher, danach monatlich
|
||||
minTermMonths: 12,
|
||||
minutesPerMonth: 60,
|
||||
creditValidityMonths: 12,
|
||||
usageRoundingMinutes: 15,
|
||||
|
||||
+29
-9
@@ -34,6 +34,21 @@ function addDays(iso, days) {
|
||||
return d
|
||||
}
|
||||
|
||||
function addMonthsIso(iso, months) {
|
||||
const d = new Date(iso)
|
||||
const day = d.getUTCDate()
|
||||
d.setUTCDate(1)
|
||||
d.setUTCMonth(d.getUTCMonth() + months)
|
||||
const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate()
|
||||
d.setUTCDate(Math.min(day, last))
|
||||
return d.toISOString()
|
||||
}
|
||||
|
||||
function minTermEndOf(sub) {
|
||||
const start = sub?.trialEnd || sub?.startedAt
|
||||
return start ? addMonthsIso(start, config.abo.minTermMonths) : null
|
||||
}
|
||||
|
||||
function publicSubscription(sub) {
|
||||
if (!sub) return null
|
||||
const active = ACTIVE_STATUSES.has(sub.status)
|
||||
@@ -45,6 +60,8 @@ function publicSubscription(sub) {
|
||||
startedAt: sub.startedAt,
|
||||
currentPeriodEnd: sub.currentPeriodEnd,
|
||||
cancelAtPeriodEnd: Boolean(sub.cancelAtPeriodEnd),
|
||||
cancelAt: sub.cancelAt || (sub.cancelAtPeriodEnd ? sub.currentPeriodEnd : null),
|
||||
minTermEnd: minTermEndOf(sub),
|
||||
endedAt: sub.endedAt,
|
||||
priceAmount: sub.priceAmount,
|
||||
startChoice: sub.startChoice || null,
|
||||
@@ -162,7 +179,8 @@ async function sendContractConfirmation(subDoc) {
|
||||
' Preis: 50,00 € pro Monat (gemäß § 19 UStG ohne Umsatzsteuer)',
|
||||
` Vertragsschluss: ${fmtDate(subDoc.startedAt)}`,
|
||||
` Leistungsbeginn: ${lateStart ? fmtDate(subDoc.trialEnd) : fmtDate(subDoc.startedAt)}`,
|
||||
' Laufzeit: unbestimmt, keine Mindestlaufzeit, jederzeit zum Ende des Abrechnungsmonats kündbar',
|
||||
` Laufzeit: Mindestlaufzeit 12 Monate bis ${fmtDate(minTermEndOf(subDoc))}; Kündigung zu diesem Termin bis einen Monat vorher,`,
|
||||
' danach läuft das Abo unbefristet und ist jederzeit zum Ende des Abrechnungsmonats kündbar',
|
||||
' Enthalten: Betrieb und Pflege Ihrer Website bzw. Web-Anwendung, 1 Domain, 1 E-Mail-Postfach,',
|
||||
' 1 Stunde Arbeitszeit je bezahltem Monat (12 Monate nutzbar)',
|
||||
'',
|
||||
@@ -194,13 +212,13 @@ async function sendContractConfirmation(subDoc) {
|
||||
/** Kuendigung ueber "Abo verwalten" (Stripe-Kundenportal): Bestaetigung in Textform + Hinweis an uns. */
|
||||
async function sendPortalCancellationConfirmation(sub, createdUnix) {
|
||||
const receivedAt = new Date((createdUnix || Date.now() / 1000) * 1000)
|
||||
const endsAt = sub.currentPeriodEnd
|
||||
const endsAt = sub.cancelAt || sub.currentPeriodEnd
|
||||
await sendCustomerMail(sub.email, 'Bestätigung Ihrer Kündigung – WEBklar Wartungs-Abo', [
|
||||
'Guten Tag,',
|
||||
'',
|
||||
`wir bestätigen Ihre Kündigung des WEBklar Wartungs-Abos, eingegangen am ${fmtDateTime(receivedAt)} über „Abo verwalten“ im Kundenportal.`,
|
||||
'',
|
||||
`Ihr Wartungs-Abo endet zum ${fmtDate(endsAt)} (Ende des Abrechnungsmonats). Bis dahin bleiben alle Leistungen verfügbar,`,
|
||||
`Ihr Wartungs-Abo endet zum ${fmtDate(endsAt)}. Bis dahin bleiben alle Leistungen verfügbar,`,
|
||||
'danach erfolgen keine weiteren Abbuchungen. Nicht genutzte Zeit aus dem Stundenkontingent entfällt mit Vertragsende.',
|
||||
'Ihre Daten, den Domain-AuthCode und Ihr Postfach stellen wir Ihnen auf Wunsch gemäß § 9 der Vertragsbedingungen bereit.',
|
||||
'',
|
||||
@@ -330,7 +348,7 @@ function validDeclaration(body) {
|
||||
name,
|
||||
email,
|
||||
customerNumber: String(body?.customerNumber || '').trim().slice(0, 50),
|
||||
kind: body?.kind === 'ausserordentlich' ? 'ausserordentlich' : 'ordentlich',
|
||||
kind: ['ausserordentlich', 'geschaeftsaufgabe'].includes(body?.kind) ? body.kind : 'ordentlich',
|
||||
reason: String(body?.reason || '').trim().slice(0, 2000),
|
||||
effective: /^\d{4}-\d{2}-\d{2}$/.test(String(body?.effective || '')) ? String(body.effective) : '',
|
||||
}
|
||||
@@ -355,9 +373,9 @@ router.post('/public/cancel', async (req, res) => {
|
||||
let effectiveAt = null
|
||||
try {
|
||||
sub = await findActiveSubscriptionByEmail(d.email)
|
||||
if (sub && d.kind === 'ordentlich') {
|
||||
if (sub && d.kind !== 'ausserordentlich') {
|
||||
const requested = d.effective ? new Date(`${d.effective}T12:00:00Z`) : null
|
||||
const result = await cancelSubscriptionAt(sub.stripeSubscriptionId, requested, 'kuendigungsbutton')
|
||||
const result = await cancelSubscriptionAt(sub.stripeSubscriptionId, requested, 'kuendigungsbutton', d.kind, receivedAt)
|
||||
sub = result.subscription
|
||||
effectiveAt = result.effectiveAt
|
||||
}
|
||||
@@ -370,8 +388,9 @@ router.post('/public/cancel', async (req, res) => {
|
||||
console.error('[abo] Kuendigung automatisch nicht moeglich:', err.message)
|
||||
}
|
||||
|
||||
const kindLabel = { ordentlich: 'ordentliche Kündigung', ausserordentlich: 'außerordentliche Kündigung', geschaeftsaufgabe: 'Sonderkündigung wegen Geschäftsaufgabe' }[d.kind]
|
||||
const effectiveText = effectiveAt
|
||||
? `Ihr Wartungs-Abo endet zum ${fmtDate(effectiveAt)} (Ende des Abrechnungsmonats).`
|
||||
? `Ihr Wartungs-Abo endet zum ${fmtDate(effectiveAt)}.${d.kind === 'geschaeftsaufgabe' ? ' Bitte senden Sie uns innerhalb von zwei Wochen einen Nachweis der Geschäftsaufgabe (z. B. Gewerbeabmeldung) an kenso.gri@gmail.com.' : ''}`
|
||||
: d.kind === 'ausserordentlich'
|
||||
? 'Ihre außerordentliche Kündigung prüfen wir umgehend und bestätigen Ihnen das Vertragsende gesondert.'
|
||||
: 'Wir ordnen Ihre Kündigung Ihrem Vertrag zu und bestätigen Ihnen das Vertragsende gesondert.'
|
||||
@@ -379,7 +398,7 @@ router.post('/public/cancel', async (req, res) => {
|
||||
await sendCustomerMail(d.email, 'Eingangsbestätigung Ihrer Kündigung – Webklar Wartungs-Abo', [
|
||||
`Guten Tag ${d.name},`,
|
||||
'',
|
||||
`wir haben Ihre ${d.kind === 'ausserordentlich' ? 'außerordentliche' : 'ordentliche'} Kündigung am ${fmtDateTime(receivedAt)} erhalten.`,
|
||||
`wir haben Ihre ${kindLabel} am ${fmtDateTime(receivedAt)} erhalten.`,
|
||||
'',
|
||||
` Name: ${d.name}`,
|
||||
` E-Mail: ${d.email}`,
|
||||
@@ -398,7 +417,7 @@ router.post('/public/cancel', async (req, res) => {
|
||||
|
||||
await sendAdminNotice(`Kündigung eingegangen: ${d.email}`, [
|
||||
`${d.name} <${d.email}> ${d.customerNumber ? `(Kd.-Nr. ${d.customerNumber})` : ''}`,
|
||||
`Art: ${d.kind}, Zeitpunkt: ${d.effective || 'nächstmöglich'}`,
|
||||
`Art: ${kindLabel}, gewünschter Zeitpunkt: ${d.effective || 'nächstmöglich'}`,
|
||||
d.reason ? `Grund: ${d.reason}` : '',
|
||||
effectiveAt
|
||||
? `Automatisch in Stripe zum ${fmtDate(effectiveAt)} gekündigt (${sub.stripeSubscriptionId}).`
|
||||
@@ -406,6 +425,7 @@ router.post('/public/cancel', async (req, res) => {
|
||||
? `Abo ${sub.stripeSubscriptionId} gefunden, aber NICHT automatisch gekündigt (außerordentlich) -> prüfen und Vertragsende bestätigen!`
|
||||
: 'KEIN aktives Abo zu dieser E-Mail gefunden -> bitte manuell zuordnen und Vertragsende bestätigen!',
|
||||
d.kind === 'ausserordentlich' ? 'Außerordentliche Kündigung -> Grund prüfen, ggf. sofort beenden.' : '',
|
||||
d.kind === 'geschaeftsaufgabe' ? 'Sonderkündigung Geschäftsaufgabe -> Nachweis (z. B. Gewerbeabmeldung) anfordern/prüfen; ohne Nachweis Kündigungstermin in Stripe auf Ende der Mindestlaufzeit setzen.' : '',
|
||||
])
|
||||
|
||||
return res.json({ ok: true, receivedAt: receivedAt.toISOString() })
|
||||
|
||||
@@ -93,14 +93,8 @@ export async function ensurePortalConfiguration() {
|
||||
invoice_history: { enabled: true },
|
||||
payment_method_update: { enabled: true },
|
||||
customer_update: { enabled: true, allowed_updates: ['address', 'name', 'email'] },
|
||||
subscription_cancel: {
|
||||
enabled: true,
|
||||
mode: 'at_period_end',
|
||||
cancellation_reason: {
|
||||
enabled: true,
|
||||
options: ['too_expensive', 'unused', 'switched_service', 'other'],
|
||||
},
|
||||
},
|
||||
// Kuendigung nur ueber die eigene Kuendigungsseite: Stripe kennt die Mindestlaufzeit nicht
|
||||
subscription_cancel: { enabled: false },
|
||||
},
|
||||
metadata: { webklar: 'wartungs-abo' },
|
||||
})
|
||||
@@ -170,6 +164,7 @@ export async function syncSubscription(subscriptionId) {
|
||||
startedAt: toIso(sub.start_date),
|
||||
currentPeriodEnd: toIso(periodEndOf(sub)),
|
||||
cancelAtPeriodEnd: Boolean(sub.cancel_at_period_end || sub.cancel_at),
|
||||
cancelAt: sub.cancel_at ? toIso(sub.cancel_at) : sub.cancel_at_period_end ? toIso(periodEndOf(sub)) : null,
|
||||
canceledAt: toIso(sub.canceled_at),
|
||||
endedAt: toIso(sub.ended_at),
|
||||
termsVersion: meta.termsVersion || null,
|
||||
@@ -185,11 +180,50 @@ export async function syncSubscription(subscriptionId) {
|
||||
}
|
||||
|
||||
function addMonths(date, months) {
|
||||
// Monatsende abfangen (31.01. + 1 Monat = 28./29.02., wie bei Stripe)
|
||||
const d = new Date(date)
|
||||
const day = d.getUTCDate()
|
||||
d.setUTCDate(1)
|
||||
d.setUTCMonth(d.getUTCMonth() + months)
|
||||
const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate()
|
||||
d.setUTCDate(Math.min(day, last))
|
||||
return d
|
||||
}
|
||||
|
||||
/**
|
||||
* Beendigungszeitpunkt nach § 8 der Bedingungen (Version 3):
|
||||
* - Leistungsbeginn = Ende der Probezeit (Beginn nach Widerrufsfrist) bzw. Abo-Start
|
||||
* - Mindestlaufzeit 12 Abrechnungsmonate; Kuendigung bis 1 Monat vor deren Ende wirkt zum Ende,
|
||||
* spaeter eingehende Kuendigungen zum Ende des darauffolgenden Abrechnungsmonats
|
||||
* - nach der Mindestlaufzeit: zum Ende des laufenden Abrechnungsmonats
|
||||
* - Geschaeftsaufgabe: 1 Monat Frist zum Ende eines Abrechnungsmonats
|
||||
* - Wunschtermin: fruehestens zum berechneten Zeitpunkt, sonst zum Ende des Abrechnungsmonats,
|
||||
* in dem der Wunschtermin liegt
|
||||
*/
|
||||
export function computeCancellationDate({ serviceStart, receivedAt = new Date(), requestedDate = null, kind = 'ordentlich' }) {
|
||||
const start = new Date(serviceStart)
|
||||
const boundary = (k) => addMonths(start, k)
|
||||
const firstBoundaryFrom = (minK, notBefore) => {
|
||||
let k = Math.max(1, minK)
|
||||
while (boundary(k) < notBefore) k += 1
|
||||
return k
|
||||
}
|
||||
const minTermK = config.abo.minTermMonths
|
||||
let k
|
||||
if (kind === 'geschaeftsaufgabe') {
|
||||
k = firstBoundaryFrom(1, addMonths(receivedAt, 1))
|
||||
} else {
|
||||
const deadline = addMonths(boundary(minTermK), -1)
|
||||
k = receivedAt <= deadline ? minTermK : firstBoundaryFrom(minTermK + 1, receivedAt)
|
||||
}
|
||||
if (requestedDate && requestedDate > boundary(k)) k = firstBoundaryFrom(k, requestedDate)
|
||||
return { effectiveAt: boundary(k), minTermEnd: boundary(minTermK), serviceStart: start }
|
||||
}
|
||||
|
||||
export function serviceStartOf(sub) {
|
||||
return new Date(((sub.trial_end || sub.start_date) || 0) * 1000)
|
||||
}
|
||||
|
||||
function subscriptionIdOfInvoice(invoice) {
|
||||
return (
|
||||
invoice.subscription ||
|
||||
@@ -333,8 +367,8 @@ export async function createAboCheckout(customer, consent) {
|
||||
custom_text: {
|
||||
submit: {
|
||||
message: lateStart
|
||||
? 'Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit dem Abschluss kommt der Vertrag zustande. Wie gewählt beginnen wir nach Ablauf der Widerrufsfrist – bis dahin erbringen wir keine Leistungen und buchen nichts ab. Danach 50,00 € monatlich, monatlich kündbar.'
|
||||
: 'Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit der ersten Zahlung kommt der Vertrag zustande. 50,00 € monatlich, monatlich kündbar.',
|
||||
? 'Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit dem Abschluss kommt der Vertrag zustande. Wie gewählt beginnen wir nach Ablauf der Widerrufsfrist – bis dahin erbringen wir keine Leistungen und buchen nichts ab. Danach 50,00 € monatlich, Mindestlaufzeit 12 Monate, danach monatlich kündbar.'
|
||||
: 'Ihre Bestellung haben Sie im Kundenportal abgegeben. Hier hinterlegen Sie die Zahlungsart; mit der ersten Zahlung kommt der Vertrag zustande. 50,00 € monatlich, Mindestlaufzeit 12 Monate, danach monatlich kündbar.',
|
||||
},
|
||||
},
|
||||
success_url: `${config.publicBaseUrl}/dashboard.html?abo=success`,
|
||||
@@ -382,35 +416,29 @@ export async function cancelAtPeriodEnd(subscriptionId, source = 'system') {
|
||||
return syncSubscription(subscriptionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Kuendigung zu einem Wunschtermin: wirksam zum Ende des Abrechnungsmonats, in dem
|
||||
* der Wunschtermin liegt (ohne Wunschtermin: Ende des laufenden Monats).
|
||||
* Liefert das Abo-Dokument und den Beendigungszeitpunkt (ISO).
|
||||
*/
|
||||
export async function cancelSubscriptionAt(subscriptionId, requestedDate = null, source = 'kuendigungsbutton') {
|
||||
/** Kuendigung nach § 8 (Mindestlaufzeit, Wunschtermin, Geschaeftsaufgabe). */
|
||||
export async function cancelSubscriptionAt(subscriptionId, requestedDate = null, source = 'kuendigungsbutton', kind = 'ordentlich', receivedAt = new Date()) {
|
||||
const stripe = getStripe()
|
||||
const sub = await stripe.subscriptions.retrieve(subscriptionId)
|
||||
const { effectiveAt, minTermEnd } = computeCancellationDate({
|
||||
serviceStart: serviceStartOf(sub),
|
||||
receivedAt,
|
||||
requestedDate,
|
||||
kind,
|
||||
})
|
||||
const periodEnd = periodEndOf(sub)
|
||||
const requested = requestedDate ? Math.floor(requestedDate.getTime() / 1000) : null
|
||||
if (!requested || !periodEnd || requested <= periodEnd) {
|
||||
const effectiveUnix = Math.floor(effectiveAt.getTime() / 1000)
|
||||
if (periodEnd && Math.abs(effectiveUnix - periodEnd) < 3600) {
|
||||
await stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: true, metadata: { cancelSource: source } })
|
||||
} else {
|
||||
await stripe.subscriptions.update(subscriptionId, {
|
||||
cancel_at_period_end: true,
|
||||
cancel_at: effectiveUnix,
|
||||
proration_behavior: 'none',
|
||||
metadata: { cancelSource: source },
|
||||
})
|
||||
const doc = await syncSubscription(subscriptionId)
|
||||
return { subscription: doc, effectiveAt: toIso(periodEnd) }
|
||||
}
|
||||
let boundary = new Date(periodEnd * 1000)
|
||||
for (let i = 0; i < 36 && boundary.getTime() / 1000 < requested; i += 1) {
|
||||
boundary = addMonths(new Date(periodEnd * 1000), i + 1)
|
||||
}
|
||||
await stripe.subscriptions.update(subscriptionId, {
|
||||
cancel_at: Math.floor(boundary.getTime() / 1000),
|
||||
proration_behavior: 'none',
|
||||
metadata: { cancelSource: source },
|
||||
})
|
||||
const doc = await syncSubscription(subscriptionId)
|
||||
return { subscription: doc, effectiveAt: boundary.toISOString() }
|
||||
return { subscription: doc, effectiveAt: effectiveAt.toISOString(), minTermEnd: minTermEnd.toISOString() }
|
||||
}
|
||||
|
||||
export function constructWebhookEvent(rawBody, signature) {
|
||||
|
||||
Reference in new issue
Block a user