Merge kn/recht (P8 Rechtsrunde Fassung 6) in feature/kundenansicht-neu

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-16 16:59:27 +00:00
commit 72c5fa9fea
39 files changed
+5772 -35

No files matched your search

+3
View File
@@ -920,6 +920,9 @@ export const config = {
abo: {
// Bei jeder Textaenderung an Bedingungen oder Bestellseite hochziehen (wird mit der Zustimmung gespeichert)
termsVersion: '2026-09-30-v5',
// Fassung 6 der Abo-Bedingungen: gilt nur fuer Sitzungen mit neuer Ansicht (aktuelleAboFassung in products.js).
// Entwurfstag; bei der Freigabe durch den Inhaber das Datum auf den Freigabetag setzen (P8 Task 11, Ablauf)
termsVersionNeu: '2026-10-16-v6',
// Mindestlaufzeit ab Leistungsbeginn; Kuendigung zum Ende bis 1 Monat vorher, danach monatlich
minTermMonths: products.wartung.minTermMonths,
minutesPerMonth: products.wartung.minutesPerMonth,
+128 -22
View File
@@ -1,6 +1,7 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { config } from '../config.js'
import { neueAnsicht } from './kundenansicht/schalter.js'
/**
* Abo-Produkte (Hosting-Abo, Wartungs-Abo) und E-Mail-Zusatzspeicher.
@@ -28,43 +29,86 @@ export function getProduct(key) {
/** Die archivierte Fassung 3 (public/vertragsunterlagen/v3): gilt fuer jede Zustimmung ohne Fassung "-v4"/"-v5". */
export const TERMS_VERSION_V3 = '2026-09-25-v3'
/**
* Fassungstabelle der Abo-Bedingungen (V12). Reihenfolge = Suche: der erste Eintrag, dessen muster auf die
* gespeicherte Fassung passt (nur als Endung, nie "-v60" oder "v6-..."), sonst der Eintrag sonst (Fassung 3).
* Fassung 6 liegt bis zum Umschalten (P10) im Vorschaupfad /neu/rechtliches/; Fassung 5 bleibt an den heutigen
* Adressen, archiv nennt die Adressen der Kopie unter public/vertragsunterlagen/v5/ (erst P10 schaltet sie ein).
* endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) fuer die Mails K02/K03/T05/T07 (F08).
*/
export const FASSUNGEN = Object.freeze([
Object.freeze({
muster: /-v6$/,
nummer: 6,
url: '/neu/rechtliches/abo-bedingungen.html',
pdfFile: 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
}),
Object.freeze({
muster: /-v5$/,
nummer: 5,
url: '/abo-bedingungen.html',
pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
archiv: Object.freeze({
url: '/vertragsunterlagen/v5/abo-bedingungen.html',
pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf',
}),
}),
Object.freeze({
muster: /-v4$/,
nummer: 4,
url: '/vertragsunterlagen/v4/abo-bedingungen.html',
pdfFile: 'vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
}),
Object.freeze({
sonst: true,
nummer: 3,
url: '/vertragsunterlagen/v3/abo-bedingungen.html',
pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 9',
}),
])
/** Erster Tabelleneintrag mit passendem muster; ohne Treffer (auch bei Nicht-Text) der Eintrag sonst. */
function fassungFuer(termsVersion) {
const treffer = typeof termsVersion === 'string' ? FASSUNGEN.find((f) => f.muster && f.muster.test(termsVersion)) : null
return treffer || FASSUNGEN.find((f) => f.sonst)
}
/**
* Abo-Fassung der Sitzung: Fassung 6 nur bei neuer Ansicht (Pilot-Schalter), sonst die bisherige Fassung 5.
* req wie bei neueAnsicht(req) (req.session oder signiertes Cookie).
*/
export function aktuelleAboFassung(req) {
return neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion
}
/** Aktuelle Fassung 5 (Unterstuetzung in § 4 Abs. 4)? */
export function isCurrentTerms(termsVersion) {
return typeof termsVersion === 'string' && termsVersion.endsWith('-v5')
}
/** Fassung 4 oder neuer (Hosting- und Wartungs-Abo, Zusatzspeicher, Wechsel)? Aeltere und fehlende Fassungen: nein. */
/** Fassung 4 oder neuer (Hosting- und Wartungs-Abo, Zusatzspeicher, Wechsel; v4, v5, v6)? Aeltere und fehlende Fassungen: nein. */
export function isTermsV4OrNewer(termsVersion) {
return typeof termsVersion === 'string' && /-v[45]$/.test(termsVersion)
return typeof termsVersion === 'string' && FASSUNGEN.some((f) => f.muster && f.muster.test(termsVersion))
}
/**
* Vertragsunterlagen zur zugestimmten Fassung: Fassung 5 (Suffix "-v5") zeigt die aktuellen Bedingungen samt PDF,
* Fassung 4 das Archiv unter public/vertragsunterlagen/v4, jede aeltere Version (auch unbekannt/keine) das Archiv
* der Version 3.
* Vertragsunterlagen zur zugestimmten Fassung (Tabelle FASSUNGEN): Fassung 6 (Suffix "-v6") zeigt die neuen Bedingungen
* im Vorschaupfad, Fassung 5 die bisherigen Bedingungen samt PDF, Fassung 4 das Archiv unter public/vertragsunterlagen/v4,
* jede aeltere Version (auch unbekannt/keine) das Archiv der Version 3.
* version: die Fassung, die eine Mail nennt - die gespeicherte, ohne gespeicherte die archivierte Fassung 3,
* damit Versionsangabe und PDF-Anhang immer zusammenpassen (Task 12 Fix 1).
* endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) in dieser Fassung, fuer die
* Mails K02/K03/T05/T07 (F08) - ab Fassung 4 ist § 9 der Wechsel zwischen den Abos, § 11 das Vertragsende.
* Die Rueckgabe hat immer nur diese vier Felder (kein archiv, muster, nummer).
*/
export function termsDocFor(termsVersion) {
if (isCurrentTerms(termsVersion)) {
return { version: termsVersion, url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11' }
}
if (isTermsV4OrNewer(termsVersion)) {
return {
version: termsVersion,
url: '/vertragsunterlagen/v4/abo-bedingungen.html',
pdfFile: 'vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
}
}
return {
version: typeof termsVersion === 'string' && termsVersion ? termsVersion : TERMS_VERSION_V3,
url: '/vertragsunterlagen/v3/abo-bedingungen.html',
pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 9',
}
const f = fassungFuer(termsVersion)
const version = typeof termsVersion === 'string' && termsVersion ? termsVersion : TERMS_VERSION_V3
return { version, url: f.url, pdfFile: f.pdfFile, endParagraph: f.endParagraph }
}
/** Katalogname ohne "WEBklar " (z. B. "Hosting-Abo") fuer {{produkt}} in den Mails (F33); ohne Schluessel "Abo". */
@@ -170,3 +214,65 @@ export function creditMinutesForInvoice(invoice, priceKeys) {
}
return 0
}
/* ---------------------------------------------------------------------------------------------
* Fassung 6 (Entwurf bis zur Freigabe, E20): Wortlaute fuer die Bestellseiten und Einschalt-Seiten
* sowie die Unterlagen-Tabelle der Funktionen. Dieses Paket liefert die Konstanten; verdrahtet werden
* sie erst mit P5/P7 (Nachtrag N33). Texte in "du"-Form, ohne Preise.
* ------------------------------------------------------------------------------------------- */
/** Fassung der Funktionsunterlagen (Leistungsbeschreibungen, AVV). Entwurfstag; bei der Freigabe das Datum setzen. */
export const FUNKTIONEN_FASSUNG_V6 = '2026-10-16'
/**
* Wortlaute der Oberflaeche. linkzeile: genau ein Platzhalter {fassung} (mitFassung fuellt ihn);
* bestellen gilt fuer Abo, Wechsel und Zusatzspeicher. beginn: das Haekchen nur fuer Privatkunden in der Frist.
* hinweis: die Pflichten der alten Haekchen verantwortung, stripe, gesundheit als ein Satz auf der Einschalt-Seite.
*/
export const WORTLAUT_V6 = Object.freeze({
linkzeile: Object.freeze({
bestellen: 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung {fassung}). Alles kommt danach auch als PDF per Mail.',
einschaltenMitAvv: 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung {fassung}).',
einschaltenOhneAvv: 'Es gilt die Leistungsbeschreibung (Fassung {fassung}).',
}),
beginn: Object.freeze({
abo: 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
wechsel: 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
einrichtung: 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.',
}),
hinweis: Object.freeze({
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
}),
})
/** Ersetzt {fassung} im Wortlaut; wirft bei leerer Fassung oder wenn der Text keinen Platzhalter enthaelt. */
export function mitFassung(text, fassung) {
if (typeof fassung !== 'string' || fassung.trim() === '') throw new Error('Fassung fehlt oder ist leer')
if (typeof text !== 'string' || !text.includes('{fassung}')) throw new Error('Platzhalter {fassung} fehlt im Text')
return text.split('{fassung}').join(fassung)
}
/** Ein Dokument der Funktionsunterlagen im Vorschaupfad: Seite, PDF und Dateiname des Mail-Anhangs. */
function dok(slug, anhang) {
return Object.freeze({
version: FUNKTIONEN_FASSUNG_V6,
url: `/neu/rechtliches/${slug}-${FUNKTIONEN_FASSUNG_V6}.html`,
pdfFile: `neu/rechtliches/${slug}-${FUNKTIONEN_FASSUNG_V6}.pdf`,
anhangName: `${anhang}-${FUNKTIONEN_FASSUNG_V6}.pdf`,
})
}
/**
* Unterlagen je Funktion in Fassung 6: { [key]: { lb, avv } }. avv ist null, wo keine personenbezogenen Daten im
* Auftrag verarbeitet werden (News, Oeffnungszeiten live). Reihenfolge: news, termine, shop, unterkunft, zeiten, analyse.
*/
export const FUNKTIONEN_UNTERLAGEN_V6 = Object.freeze({
news: Object.freeze({ lb: dok('leistungsbeschreibung-news', 'Leistungsbeschreibung-News-Pinnwand'), avv: null }),
termine: Object.freeze({ lb: dok('leistungsbeschreibung-terminplaner', 'Leistungsbeschreibung-Terminplaner'), avv: dok('avv-terminplaner', 'AVV-Terminplaner') }),
shop: Object.freeze({ lb: dok('leistungsbeschreibung-webshop', 'Leistungsbeschreibung-Webshop'), avv: dok('avv-webshop', 'AVV-Webshop') }),
unterkunft: Object.freeze({ lb: dok('leistungsbeschreibung-unterkunft', 'Leistungsbeschreibung-Buchungskalender'), avv: dok('avv-unterkunft', 'AVV-Buchungskalender') }),
zeiten: Object.freeze({ lb: dok('leistungsbeschreibung-zeiten', 'Leistungsbeschreibung-Oeffnungszeiten-live'), avv: null }),
analyse: Object.freeze({ lb: dok('leistungsbeschreibung-website-analyse', 'Leistungsbeschreibung-Website-Analyse'), avv: dok('avv-website-analyse', 'AVV-Website-Analyse') }),
})