/** * Sätze der Bestellseiten (Abo bestellen, Wechsel, Zusatzspeicher-Plus, Antrag auf Übernahmegutschrift) in Fassung 6. * * ENTWURF, Stand 16.10.2026: Der Wortlaut ist nicht freigegeben (E20). Stellt der Inhaber ihn frei, wird nur WORTLAUT_STATUS auf * 'freigegeben' umgestellt; Code und Oberfläche lesen den Status aus dieser Datei. * * Quelle von Linkzeile und Häkchen ist WORTLAUT_V6 in products.js (P8). Diese Datei bleibt ohne Imports, damit Server, Tests und * Werkzeug sie ohne Umgebung laden; sie hält die beiden Sätze als Kopie, und test/knBestelltexte.test.js sowie * test/knVerdrahtung.test.js binden die Gleichheit mit WORTLAUT_V6 und mitFassung. Ändert sich der Wortlaut dort, werden diese * Tests rot und die Kopie hier zieht nach. Die übrigen Sätze (Sperrzeile, Warnzeile, Gutschrift-Satz, Kundenart-Zeile, Fehlertexte) * stehen nur hier und in docs/rechtliches/kundenansicht-entscheidungen.md. * * Markierungen: In den Vorlagen stehen Link-Texte in eckigen Klammern. klartext() entfernt die Klammern; die Adressen dazu stehen * als urls in derselben Reihenfolge. Die Oberfläche setzt daraus Links (nie HTML aus dem Text), der Nachweis speichert den Klartext. * * Fassungsangabe: Alle Sätze nennen die Fassung als Versionszeichenfolge (z. B. 2026-10-16-v6), so wie mitFassung sie einsetzt. */ /** 'entwurf' | 'freigegeben'; stellt nur der Inhaber (P8) um. */ export const WORTLAUT_STATUS = 'entwurf' /** Text des Knopfs, wörtlich (rechtlich fest, Spec 4). */ export const KNOPF = 'Zahlungspflichtig bestellen' /** Knopf des Antrags auf Übernahmegutschrift. */ export const KNOPF_ANTRAG = 'Antrag absenden' export const WECHSEL_GESPERRT = 'Der Wechsel gilt sofort. Ohne dein Häkchen können wir ihn nicht ausführen.' export const FEHLER_NACHWEIS = 'Das hat gerade nicht geklappt. Bitte versuche es gleich noch einmal.' export const FEHLER_BEDINGUNGEN = 'Die Bedingungen wurden aktualisiert. Bitte lade die Seite neu.' export const FEHLER_UNVOLLSTAENDIG = 'Die Bestellung ist unvollständig. Bitte lade die Seite neu.' const DATENSCHUTZ = '/datenschutz.html' // Kopie aus WORTLAUT_V6 (products.js); {fassung} füllt bestelltexteFuerClient mit der Versionszeichenfolge const LINKZEILE = 'Es gelten die [Vertragsbedingungen] mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung {fassung}). Alles kommt danach auch als PDF per Mail. [Datenschutz]' const HAKEN_ABO = 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.' const HAKEN_WECHSEL = 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.' const WARNUNG_SPEICHER = 'Wichtig: Passen die gespeicherten E-Mails nach der Verkleinerung nicht mehr in den verbleibenden Speicher, nehmen die Postfächer keine neuen E-Mails mehr an. Bitte räume vorher auf. Mehr dazu in den [Regeln zum Zusatzspeicher].' const SATZ_GUTSCHRIFT = 'Mit dem Absenden bestätigst du, dass deine Angaben stimmen und die Rechnung auf dich bzw. dein Unternehmen ausgestellt ist. Es gelten die [Bedingungen der Übernahmegutschrift]. Angaben und Nachweise verwendet WEBklar zur Prüfung und Abwicklung des Antrags ([Datenschutz]).' const KUNDENART_BETRIEB = 'Du bestellst als Betrieb' const KUNDENART_PRIVAT = 'Du bestellst privat' const KUNDENART_KORREKTUR = 'Stimmt nicht? [Schreib uns im Chat]' const ARTEN = ['abo', 'wechsel', 'speicher'] /** 'Es gilt [A] und [B].' wird zu 'Es gilt A und B.' */ export function klartext(vorlage) { if (typeof vorlage !== 'string') throw new TypeError('Vorlage muss ein Text sein') return vorlage.replace(/\[([^\]]*)\]/g, '$1') } function fassungPruefen(fassung) { if (typeof fassung !== 'string' || fassung.trim() === '') throw new Error('Fassung fehlt oder ist leer') return fassung } const mitFassung = (vorlage, fassung) => vorlage.split('{fassung}').join(fassungPruefen(fassung)) /** * Nutzlast für GET /api/abo (neue Ansicht): die Sätze der vier Seiten mit den Adressen ihrer Link-Texte. * @param {{ fassung: string, termsUrl: string }} p fassung: Versionszeichenfolge (config.abo.termsVersionNeu); termsUrl: Adresse der Bedingungen */ export function bestelltexteFuerClient({ fassung, termsUrl }) { fassungPruefen(fassung) if (typeof termsUrl !== 'string' || termsUrl === '') throw new Error('Adresse der Bedingungen fehlt') const linkzeile = { text: mitFassung(LINKZEILE, fassung), urls: [termsUrl, DATENSCHUTZ] } return { status: WORTLAUT_STATUS, fassung, knopf: KNOPF, abo: { linkzeile, haken: HAKEN_ABO }, wechsel: { linkzeile: { text: linkzeile.text, urls: [...linkzeile.urls] }, haken: HAKEN_WECHSEL, gesperrt: WECHSEL_GESPERRT }, speicher: { linkzeile: { text: linkzeile.text, urls: [`${termsUrl}#zusatzspeicher`, DATENSCHUTZ] }, warnung: { text: WARNUNG_SPEICHER, urls: [`${termsUrl}#zusatzspeicher`] }, }, gutschrift: { satz: { text: SATZ_GUTSCHRIFT, urls: ['#gutschrift?bedingungen=1', `${DATENSCHUTZ}#uebernahmegutschrift`] }, knopf: KNOPF_ANTRAG }, kundenart: { unternehmen: KUNDENART_BETRIEB, privat: KUNDENART_PRIVAT, korrektur: { text: KUNDENART_KORREKTUR, urls: ['#chat'] } }, } } function euro(cents) { if (!Number.isInteger(cents) || cents < 0) throw new TypeError('Betrag muss eine ganze Zahl in Cent sein') return `${(cents / 100).toFixed(2).replace('.', ',')} €` } function datum(iso) { const ms = Date.parse(iso) if (!Number.isFinite(ms)) throw new TypeError('Datum ungültig') return new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(ms)) } function text(wert, name) { if (typeof wert !== 'string' || wert.trim() === '') throw new TypeError(`${name} fehlt`) return wert } function zahl(wert, name) { if (!Number.isInteger(wert) || wert < 1) throw new TypeError(`${name} muss eine positive ganze Zahl sein`) return wert } const UST = 'gemäß § 19 UStG ohne Umsatzsteuer' /** * Die Zeile „bestellung“ des Nachweises: eine Zusammenfassung aus Werten, die der Server selbst kennt oder geprüft hat. * @param {'abo'|'wechsel'|'speicher'} art * @param {object} fakten abo: { produkt, monatCents, mindestMonate, beginn: 'sofort'|'nach_frist', beginnAm?, angebot? }; * wechsel: { heuteCents, monatCents, mindestMonate }; speicher: { units, gb, monatCents } */ export function bestellZeile(art, fakten) { const f = fakten ?? {} if (art === 'abo') { const nachFrist = f.beginn === 'nach_frist' if (!nachFrist && f.beginn !== 'sofort') throw new TypeError('Beginn muss sofort oder nach_frist sein') const angebot = f.angebot ? `individuelles Angebot: ${text(f.angebot, 'Angebot')}, ` : '' const beginn = nachFrist ? `Beginn nach Ablauf der Widerrufsfrist am ${datum(f.beginnAm)}` : 'Beginn sofort' return `${text(f.produkt, 'Produkt')}: ${euro(f.monatCents)} pro Monat (Gesamtpreis, ${UST}), ${angebot}${beginn}, Mindestlaufzeit ${zahl(f.mindestMonate, 'Mindestlaufzeit')} Monate ab Leistungsbeginn` } if (art === 'wechsel') { return `Wechsel ins Wartungs-Abo, Beginn sofort. Heute fällig: ${euro(f.heuteCents)} (${UST}), danach ${euro(f.monatCents)} pro Monat zuzüglich gebuchtem Zusatzspeicher, Mindestlaufzeit ${zahl(f.mindestMonate, 'Mindestlaufzeit')} Monate ab dem Wechsel` } if (art === 'speicher') { const n = zahl(f.units, 'Pakete') return `Zusatzspeicher: ${n} ${n === 1 ? 'Paket' : 'Pakete'} (+${zahl(f.gb, 'Speicher')} GB), ${euro(f.monatCents)} pro Monat (${UST}), monatlich kündbar` } throw new TypeError(`Unbekannte Art: ${String(art)}`) } /** * Anzeigereihenfolge des Nachweises als Klartext: bestellung (nur bei nichtleerer Zeile), linkzeile, beginn (nur wenn gezeigt und * gesetzt), knopf, kundenart. Gutschrift: satz, knopf. * @param {'abo'|'wechsel'|'speicher'|'gutschrift'} art * @param {{ kundenart?: string, fassung?: string, bestellung?: string, beginnGehakt?: boolean }} p * kundenart: 'unternehmen' oder (alles andere) 'privat', vom Server aus dem Kundendokument; beginnGehakt: das Häkchen war gezeigt und gesetzt * @returns {Array<{ id: string, text: string, haken: boolean }>} */ export function nachweisTexte(art, { kundenart, fassung, bestellung = '', beginnGehakt = false } = {}) { if (art === 'gutschrift') { return [ { id: 'satz', text: klartext(SATZ_GUTSCHRIFT), haken: false }, { id: 'knopf', text: KNOPF_ANTRAG, haken: false }, ] } if (!ARTEN.includes(art)) throw new TypeError(`Unbekannte Art: ${String(art)}`) const betrieb = kundenart === 'unternehmen' const zeilen = [] if (typeof bestellung === 'string' && bestellung !== '') zeilen.push({ id: 'bestellung', text: bestellung, haken: false }) zeilen.push({ id: 'linkzeile', text: klartext(mitFassung(LINKZEILE, fassung)), haken: false }) if ((art === 'abo' || art === 'wechsel') && !betrieb && beginnGehakt === true) { zeilen.push({ id: 'beginn', text: art === 'abo' ? HAKEN_ABO : HAKEN_WECHSEL, haken: true }) } zeilen.push({ id: 'knopf', text: KNOPF, haken: false }) zeilen.push({ id: 'kundenart', text: `${betrieb ? KUNDENART_BETRIEB : KUNDENART_PRIVAT} · ${klartext(KUNDENART_KORREKTUR)}`, haken: false }) return zeilen }