Übernahmegutschrift und Leitfaden: für beide Abos (Entwurf)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5.1 committed 2026-09-27 20:23:44 +00:00
1 parent 0098899ac6
commit a2c1f0d81c
6 files changed
+73 -28

No files matched your search

Binary file not shown.
+25 -15
View File
@@ -4,21 +4,23 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Empfehlen für WEBklar – Leitfaden</title> <title>Empfehlen für WEBklar – Leitfaden</title>
<style> <style>
@page { size: A4; margin: 13mm 16mm 12mm; } @page { size: A4; margin: 11mm 14mm 10mm; }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { margin: 0; color: #1d1d1f; font: 10pt/1.45 "DejaVu Sans", system-ui, sans-serif; } body { margin: 0; color: #1d1d1f; font: 9.5pt/1.38 "DejaVu Sans", system-ui, sans-serif; }
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 10pt; color: #6d1fd1; } .brand { font-weight: 800; letter-spacing: 0.12em; font-size: 10pt; color: #6d1fd1; }
h1 { font-size: 20pt; line-height: 1.15; margin: 3mm 0 1.5mm; } h1 { font-size: 19pt; line-height: 1.15; margin: 2.5mm 0 1.2mm; }
.lead { font-size: 10.5pt; color: #444; margin: 0 0 4mm; } .lead { font-size: 10.5pt; color: #444; margin: 0 0 3mm; }
h2 { font-size: 12pt; margin: 4.5mm 0 2mm; } h2 { font-size: 12pt; margin: 3.5mm 0 1.5mm; }
ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 3mm; } ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 2mm; }
ol.steps li { display: grid; grid-template-columns: 9mm 1fr; gap: 3mm; align-items: start; } ol.steps li { display: grid; grid-template-columns: 9mm 1fr; gap: 3mm; align-items: start; }
.num { width: 8mm; height: 8mm; border-radius: 50%; background: #6d1fd1; color: #fff; font-weight: 700; display: flex; align-items: center; justify-content: center; } .num { width: 8mm; height: 8mm; border-radius: 50%; background: #6d1fd1; color: #fff; font-weight: 700; display: flex; align-items: center; justify-content: center; }
ol.steps strong { display: block; } ol.steps strong { display: block; }
.box { border: 1px solid #e0dccf; border-radius: 3mm; padding: 3mm 4mm; margin: 3.5mm 0; background: #faf9f6; } .box { border: 1px solid #e0dccf; border-radius: 3mm; padding: 2mm 4mm; margin: 2.5mm 0; background: #faf9f6; }
.rules { margin: 0; padding-left: 5mm; } .rules { margin: 0; padding-left: 5mm; }
.rules li { margin: 1.4mm 0; } .rules li { margin: 1mm 0; }
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 4mm; } .abos { margin: 1mm 0; padding-left: 4.5mm; }
.abos li { margin: 0.8mm 0; }
.grid2 { display: grid; grid-template-columns: 1fr 1.2fr; gap: 4mm; }
.muted { color: #666; font-size: 9pt; } .muted { color: #666; font-size: 9pt; }
.contact { font-size: 10pt; } .contact { font-size: 10pt; }
.do::marker { content: "✓ "; color: #2f7d3b; } .do::marker { content: "✓ "; color: #2f7d3b; }
@@ -39,14 +41,22 @@
Frag den Betrieb, ob du seine Kontaktdaten an uns weitergeben darfst. Oder gib ihm unsere Kontaktdaten (unten). Frag den Betrieb, ob du seine Kontaktdaten an uns weitergeben darfst. Oder gib ihm unsere Kontaktdaten (unten).
Schick uns dann Name des Betriebs, Ansprechpartner, Telefon oder E-Mail, worum es kurz geht, und dass er mit der Weitergabe einverstanden ist.</div></li> Schick uns dann Name des Betriebs, Ansprechpartner, Telefon oder E-Mail, worum es kurz geht, und dass er mit der Weitergabe einverstanden ist.</div></li>
<li><span class="num">3</span><div><strong>Den Rest machen wir</strong> <li><span class="num">3</span><div><strong>Den Rest machen wir</strong>
Beratung, Angebot und Website übernehmen wir. Für das Wartungs-Abo bekommt der Kunde von uns einen Zugang zum Kundenportal Beratung, Angebot und Website übernehmen wir. Für das Hosting- oder Wartungs-Abo bekommt der Kunde von uns einen Zugang zum Kundenportal
und schließt es dort <b>selbst online</b> ab: ohne Unterschrift, ohne Termin, Zahlung sicher über Stripe.</div></li> und schließt es dort im Bereich „Verträge“ <b>selbst online</b> ab: ohne Unterschrift, ohne Termin, Zahlung sicher über Stripe.</div></li>
</ol> </ol>
<div class="box"> <div class="box">
<strong>Das Wartungs-Abo auf einen Blick</strong><br> <strong>Unsere Abos auf einen Blick</strong> · beide mit Mindestlaufzeit 12 Monate, danach monatlich kündbar
50 € pro Monat, Mindestlaufzeit 12 Monate, danach monatlich kündbar. Enthalten sind Betrieb und Pflege der Website, <ul class="abos">
1 Domain, 1 E-Mail-Postfach und 1 Stunde Arbeitszeit pro Monat (sammelt sich an, jede Stunde 12 Monate gültig). <li><b>Hosting-Abo, 15,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): nur statische Websites; 1 Domain,
1 E-Mail-Postfach mit 5 GB; keine Pflege der Website und keine Arbeitszeit.</li>
<li><b>Wartungs-Abo, 50,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): alle Websites und Web-Anwendungen; Betrieb und Pflege,
1 Domain, beliebig viele Postfächer mit zusammen 5 GB, 1 Stunde Arbeitszeit pro Monat (sammelt sich an, 12 Monate gültig).</li>
<li><b>In beiden Abos:</b> E-Mail-Zusatzspeicher je 5 GB für 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer) und die
Übernahmegutschrift, wenn der Kunde Domain, Hosting oder E-Mail beim bisherigen Anbieter im Voraus bezahlt hat (nach Prüfung, anteilig).</li>
<li><b>Wechsel</b> vom Hosting- ins Wartungs-Abo im Kundenportal unter „Verträge“ (Hosting-Abo läuft, nicht gekündigt, keine offene Zahlung);
es beginnt eine neue Mindestlaufzeit von 12 Monaten.</li>
</ul>
Alle Details stehen in den Abo-Bedingungen: project.webklar.com/abo-bedingungen.html Alle Details stehen in den Abo-Bedingungen: project.webklar.com/abo-bedingungen.html
</div> </div>
@@ -61,7 +71,7 @@
</ul> </ul>
<ul class="rules"> <ul class="rules">
<li class="dont">Keine Werbe-E-Mails, WhatsApp- oder SMS-Nachrichten und keine Werbeanrufe an Leute, die dem nicht zugestimmt haben.</li> <li class="dont">Keine Werbe-E-Mails, WhatsApp- oder SMS-Nachrichten und keine Werbeanrufe an Leute, die dem nicht zugestimmt haben.</li>
<li class="dont">Keine Zusagen zu Preisen, Rabatten, Terminen oder Leistungen, die nicht auf webklar.com oder in unserem Angebot stehen.</li> <li class="dont">Keine Zusagen zu Preisen, Rabatten, Terminen oder Leistungen, die nicht auf webklar.com, in den Abo-Bedingungen oder in unserem Angebot stehen.</li>
<li class="dont">Nichts für WEBklar unterschreiben, keine Verträge abschließen, kein Geld annehmen.</li> <li class="dont">Nichts für WEBklar unterschreiben, keine Verträge abschließen, kein Geld annehmen.</li>
<li class="dont">Kundendaten nicht sammeln, speichern oder anders verwenden.</li> <li class="dont">Kundendaten nicht sammeln, speichern oder anders verwenden.</li>
</ul> </ul>
+2 -2
View File
@@ -1043,7 +1043,7 @@ const TAKEOVER_STATUS = {
// Bei Textänderungen config.takeover.conditionsVersion auf dem Server hochziehen // Bei Textänderungen config.takeover.conditionsVersion auf dem Server hochziehen
const TAKEOVER_CONDITIONS = [ const TAKEOVER_CONDITIONS = [
'Die Übernahmegutschrift gibt es für Leistungen, die das Wartungs-Abo ersetzt (Domain, Hosting bzw. Betrieb der Website, E-Mail-Postfach) und die Sie bei Ihrem bisherigen Anbieter bereits im Voraus bezahlt haben.', 'Die Übernahmegutschrift gibt es für Leistungen, die Ihr Abo ersetzt (Domain, Hosting bzw. Betrieb der Website, E-Mail-Postfächer) und die Sie bei Ihrem bisherigen Anbieter bereits im Voraus bezahlt haben.',
'Wir schreiben den Anteil der Rechnung gut, der auf die Zeit ab Leistungsbeginn Ihres Abos bis zum Ende des bezahlten Zeitraums entfällt, tagesgenau berechnet.', 'Wir schreiben den Anteil der Rechnung gut, der auf die Zeit ab Leistungsbeginn Ihres Abos bis zum Ende des bezahlten Zeitraums entfällt, tagesgenau berechnet.',
'Nachweis ist die Rechnung des bisherigen Anbieters, ausgestellt auf Sie, mit Leistung, Zeitraum und Betrag. Wir können weitere Nachweise anfordern, etwa die Kündigungsbestätigung. Erstattet Ihr bisheriger Anbieter Beträge für diesen Zeitraum, teilen Sie uns das bitte mit.', 'Nachweis ist die Rechnung des bisherigen Anbieters, ausgestellt auf Sie, mit Leistung, Zeitraum und Betrag. Wir können weitere Nachweise anfordern, etwa die Kündigungsbestätigung. Erstattet Ihr bisheriger Anbieter Beträge für diesen Zeitraum, teilen Sie uns das bitte mit.',
'Die Gutschrift wird mit Ihren nächsten Monatsgebühren verrechnet. Sie wird nicht ausgezahlt und ist nicht übertragbar; ein bei Vertragsende noch nicht verrechneter Rest entfällt.', 'Die Gutschrift wird mit Ihren nächsten Monatsgebühren verrechnet. Sie wird nicht ausgezahlt und ist nicht übertragbar; ein bei Vertragsende noch nicht verrechneter Rest entfällt.',
@@ -1242,7 +1242,7 @@ async function loadTakeoverCard({ abo, notice = '' } = {}) {
const requests = data.requests || [] const requests = data.requests || []
updateTakeoverBadge(requests) updateTakeoverBadge(requests)
if (!active && !requests.length) { if (!active && !requests.length) {
card.innerHTML = '<div class="kb-panel"><p class="abo-note" style="margin:0">Die Übernahmegutschrift können Sie beantragen, sobald Ihr Wartungs-Abo läuft. <a href="#abo" data-view="abo">Zum Wartungs-Abo</a></p></div>' card.innerHTML = '<div class="kb-panel"><p class="abo-note" style="margin:0">Die Übernahmegutschrift können Sie beantragen, sobald Ihr Abo läuft. <a href="#abo" data-view="abo">Zum Abo</a></p></div>'
return return
} }
+3 -2
View File
@@ -151,8 +151,9 @@ export const config = {
// Uebernahmegutschrift: beim bisherigen Anbieter bereits bezahlte Zeit wird mit den Abo-Gebuehren verrechnet // Uebernahmegutschrift: beim bisherigen Anbieter bereits bezahlte Zeit wird mit den Abo-Gebuehren verrechnet
takeover: { takeover: {
// Bei jeder Textaenderung an den Bedingungen der Gutschrift hochziehen (wird mit dem Antrag gespeichert) // Bei jeder Textaenderung an den Bedingungen der Gutschrift hochziehen (wird mit dem Antrag gespeichert).
conditionsVersion: '2026-09-25', // Entwurf vom 27.09.2026 (Gutschrift fuer beide Abos); das Freigabedatum wird beim Livegang eingetragen.
conditionsVersion: '2026-09-27',
// Nachweise liegen ausserhalb von public/ (gitignored, ueberlebt Deploys wie preview-data/) // Nachweise liegen ausserhalb von public/ (gitignored, ueberlebt Deploys wie preview-data/)
uploadDir: process.env.TAKEOVER_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'takeover-credits'), uploadDir: process.env.TAKEOVER_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'takeover-credits'),
maxFiles: 3, maxFiles: 3,
+12 -9
View File
@@ -6,6 +6,7 @@ import { domainToASCII, domainToUnicode } from 'node:url'
import { config } from '../config.js' import { config } from '../config.js'
import { createDocument, getDocument, listDocuments, Query, updateDocument } from './appwriteAdmin.js' import { createDocument, getDocument, listDocuments, Query, updateDocument } from './appwriteAdmin.js'
import { sendAdminNotice, sendCustomerMail } from './mailer.js' import { sendAdminNotice, sendCustomerMail } from './mailer.js'
import { getProduct } from './products.js'
import { ACTIVE_STATUSES, getCurrentSubscription, getStripe, isStripeConfigured } from './stripeBilling.js' import { ACTIVE_STATUSES, getCurrentSubscription, getStripe, isStripeConfigured } from './stripeBilling.js'
/** /**
@@ -45,7 +46,7 @@ export const REQUIRED_CHECKS = [
{ key: 'recipient', label: 'Rechnung ist auf den Kunden ausgestellt (Name bzw. Firma stimmt)' }, { key: 'recipient', label: 'Rechnung ist auf den Kunden ausgestellt (Name bzw. Firma stimmt)' },
{ key: 'service', label: 'Leistung und Domain auf der Rechnung stimmen mit dem Antrag überein' }, { key: 'service', label: 'Leistung und Domain auf der Rechnung stimmen mit dem Antrag überein' },
{ key: 'period', label: 'Betrag und bezahlter Zeitraum stimmen mit der Rechnung überein' }, { key: 'period', label: 'Betrag und bezahlter Zeitraum stimmen mit der Rechnung überein' },
{ key: 'replaced', label: 'Die Leistung wird durch das Wartungs-Abo ersetzt (Umzug zu WEBklar)' }, { key: 'replaced', label: 'Die Leistung wird durch das Abo ersetzt (Umzug zu WEBklar)' },
] ]
const DAY_MS = 24 * 60 * 60 * 1000 const DAY_MS = 24 * 60 * 60 * 1000
@@ -387,7 +388,7 @@ export function customerView(doc) {
export async function createTakeoverRequest({ customer, input, uploads, consentIp }) { export async function createTakeoverRequest({ customer, input, uploads, consentIp }) {
const sub = await getCurrentSubscription(customer.$id) const sub = await getCurrentSubscription(customer.$id)
if (!isActiveSub(sub)) { if (!isActiveSub(sub)) {
throw httpError(409, 'Die Übernahmegutschrift können Sie beantragen, sobald Ihr Wartungs-Abo läuft.') throw httpError(409, 'Die Übernahmegutschrift können Sie beantragen, sobald Ihr Abo läuft.')
} }
const own = await listTakeoverRequests({ customerId: customer.$id }) const own = await listTakeoverRequests({ customerId: customer.$id })
const open = own.filter((r) => r.status === STATUS.open || r.status === STATUS.question) const open = own.filter((r) => r.status === STATUS.open || r.status === STATUS.question)
@@ -618,14 +619,14 @@ export async function runAutoChecks(doc, { allRequests, subscription, calc }) {
} }
if (!subscription) { if (!subscription) {
checks.push({ key: 'abo', status: 'warn', label: 'Kunde hat kein Wartungs-Abo – eine Gutschrift kann nicht verrechnet werden' }) checks.push({ key: 'abo', status: 'warn', label: 'Kunde hat kein Abo – eine Gutschrift kann nicht verrechnet werden' })
} else if (!isActiveSub(subscription)) { } else if (!isActiveSub(subscription)) {
checks.push({ key: 'abo', status: 'warn', label: `Wartungs-Abo ist beendet (Status ${subscription.status}) – keine Verrechnung möglich` }) checks.push({ key: 'abo', status: 'warn', label: `Abo ist beendet (Status ${subscription.status}) – keine Verrechnung möglich` })
} else { } else {
checks.push({ checks.push({
key: 'abo', key: 'abo',
status: subscription.cancelAtPeriodEnd ? 'info' : 'ok', status: subscription.cancelAtPeriodEnd ? 'info' : 'ok',
label: `Wartungs-Abo läuft, Leistungsbeginn ${fmtDate(serviceStartOf(subscription))}${subscription.cancelAtPeriodEnd ? `, gekündigt zum ${fmtDate(subscription.cancelAt || subscription.currentPeriodEnd)}` : ''}`, label: `Abo läuft, Leistungsbeginn ${fmtDate(serviceStartOf(subscription))}${subscription.cancelAtPeriodEnd ? `, gekündigt zum ${fmtDate(subscription.cancelAt || subscription.currentPeriodEnd)}` : ''}`,
}) })
} }
@@ -637,12 +638,14 @@ export async function runAutoChecks(doc, { allRequests, subscription, calc }) {
) )
} }
// Plausibilitaet gegen die Monatsgebuehr des Abos des Kunden; Abos ohne productKey (Altbestand) gelten als Wartungs-Abo
const monthly = doc.invoiceAmountCents / Math.max(1, doc.billingMonths) const monthly = doc.invoiceAmountCents / Math.max(1, doc.billingMonths)
if (monthly > config.stripe.priceAmountCents) { const aboCents = getProduct(subscription?.productKey || 'wartung').amountCents
if (monthly > aboCents) {
checks.push({ checks.push({
key: 'amount', key: 'amount',
status: 'info', status: 'info',
label: `Beim bisherigen Anbieter ${fmtEuro(monthly)} pro Monat – mehr als das Abo. Enthält die Rechnung weitere Leistungen?`, label: `Beim bisherigen Anbieter ${fmtEuro(monthly)} pro Monat – mehr als die Monatsgebühr des Abos (${fmtEuro(aboCents)}). Enthält die Rechnung weitere Leistungen?`,
}) })
} }
@@ -768,7 +771,7 @@ export async function applyTakeoverCredit(doc) {
if (!isStripeConfigured()) return { applied: false, doc, reason: 'Stripe ist nicht eingerichtet.' } if (!isStripeConfigured()) return { applied: false, doc, reason: 'Stripe ist nicht eingerichtet.' }
const sub = await getCurrentSubscription(doc.customerId) const sub = await getCurrentSubscription(doc.customerId)
if (!isActiveSub(sub) || !sub.stripeCustomerId) { if (!isActiveSub(sub) || !sub.stripeCustomerId) {
return { applied: false, doc, reason: 'Der Kunde hat kein laufendes Wartungs-Abo.' } return { applied: false, doc, reason: 'Der Kunde hat kein laufendes Abo.' }
} }
try { try {
const txn = const txn =
@@ -873,7 +876,7 @@ export async function decideTakeoverRequest(id, body, admin) {
const verified = readInvoiceFigures(body, doc) const verified = readInvoiceFigures(body, doc)
const sub = await getCurrentSubscription(doc.customerId) const sub = await getCurrentSubscription(doc.customerId)
if (!isActiveSub(sub)) { if (!isActiveSub(sub)) {
throw httpError(409, 'Der Kunde hat kein laufendes Wartungs-Abo – die Gutschrift kann nicht verrechnet werden.') throw httpError(409, 'Der Kunde hat kein laufendes Abo – die Gutschrift kann nicht verrechnet werden.')
} }
const corrections = [ const corrections = [
+31
View File
@@ -226,3 +226,34 @@ test('Datenschutz: Abos, UptimeRobot, Stand wie termsVersion', () => {
assert.equal(stand[1], `${d}.${mo}.${y}`) assert.equal(stand[1], `${d}.${mo}.${y}`)
assert.ok(read('scripts/build-legal-pdfs.sh').includes('pdf public/datenschutz.html docs/WEBklar-Datenschutzerklaerung.pdf')) assert.ok(read('scripts/build-legal-pdfs.sh').includes('pdf public/datenschutz.html docs/WEBklar-Datenschutzerklaerung.pdf'))
}) })
// --- Übernahmegutschrift, Prüftexte, Leitfaden (Plan 2, Task 3) ---
test('Übernahmegutschrift und Leitfaden: produktneutral', () => {
const app = read('public/app.js')
const cond = between(app, 'const TAKEOVER_CONDITIONS', ']')
assert.ok(!cond.includes('Wartungs-Abo') && cond.includes('Ihr Abo'), 'Bedingungen nennen „Ihr Abo“ statt des Wartungs-Abos')
assert.notEqual(config.takeover.conditionsVersion, '2026-09-25', 'conditionsVersion hochgezogen')
const tc = read('server/services/takeoverCredits.js')
assert.ok(!/Wartungs-Abo (läuft|ist beendet)|kein (laufendes )?Wartungs-Abo/.test(tc), 'Prüf-Labels und Fehlermeldungen sagen „Abo“')
assert.ok(!tc.includes('config.stripe.priceAmountCents'), 'Plausibilität gegen den Produktpreis, nicht gegen den Wartungspreis')
const lf = read('docs/leitfaden-empfehlungsgeber.html')
assert.ok(lf.includes('Hosting-Abo') && lf.includes('beliebig viele Postfächer'), 'Leitfaden nennt beide Abos')
// Festlegungen zur Umsetzung: Postfächer im Plural (Wartungs-Abo: beliebig viele), Entwurfsdatum als Version,
// Karte im Portal und Checkliste ohne Produktnamen, Preisvergleich je Produkt
assert.ok(cond.includes('E-Mail-Postfächer'))
assert.match(config.takeover.conditionsVersion, /^\d{4}-\d{2}-\d{2}$/)
assert.ok(app.includes('sobald Ihr Abo läuft') && !app.includes('sobald Ihr Wartungs-Abo läuft'))
assert.ok(!tc.includes('durch das Wartungs-Abo ersetzt'))
assert.ok(tc.includes("getProduct(subscription?.productKey || 'wartung')"))
// Leitfaden: beide Preise mit § 19 UStG, Zusatzspeicher, Wechsel, Gutschrift für beide Abos, Bestellweg „Verträge“
const lfText = strip(lf)
for (const s of ['zusammen 5 GB', 'Zusatzspeicher', 'Wechsel', 'Übernahmegutschrift', 'beiden Abos', '„Verträge“']) assert.ok(lfText.includes(s), s)
for (const preis of ['15,00 €', '50,00 €', '1,00 €']) {
const i = lfText.indexOf(preis)
assert.ok(i >= 0, preis)
assert.ok(lfText.slice(i, i + 80).includes('§ 19 UStG'), `${preis} ohne § 19 UStG`)
}
assert.ok(!lfText.includes('1 E-Mail-Postfach und 1 Stunde'), 'Wartungs-Abo nicht mehr mit einem Postfach')
assert.ok(!lfText.includes('Startseite'), 'kein veralteter Bestellweg')
})