Vertragsbedingungen Version 4: Korrekturen aus der Prüfung (Widerrufsfrist, Kündigung, Bestellübersicht, Postfächer, AVV, § 19 UStG)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f19187cc16
commit
fead8fdd5a
3 files changed
+128
-34
No files matched your search
+95
-1
@@ -12,7 +12,7 @@ const V3_SHA256 = '1c9e4b002dcd5d7896bbda24c573e56544b7123e3a88d154e5db77bd2855b
|
||||
const file = (rel) => new URL(`../${rel}`, import.meta.url)
|
||||
const read = (rel) => readFileSync(file(rel), 'utf8')
|
||||
const sha256 = (rel) => createHash('sha256').update(readFileSync(file(rel))).digest('hex')
|
||||
const strip = (html) => html.replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim()
|
||||
const strip = (html) => html.replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s+/g, ' ').trim()
|
||||
|
||||
// HTML-Ausschnitt von einem Anker bis zum naechsten, danach ohne Tags. Die Inhaltsuebersicht wiederholt
|
||||
// die Ueberschriften, deshalb wird nicht nach dem ersten Text-Treffer, sondern nach id="..." geschnitten.
|
||||
@@ -106,3 +106,97 @@ test('PDF der Fassung 4 verlinkt, erzeugt und im Build-Skript', () => {
|
||||
const erzeugt = build.split('\n').filter((l) => l.startsWith('pdf '))
|
||||
assert.ok(!erzeugt.some((l) => l.includes('WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf')), 'Fassung 3 wird nicht neu erzeugt')
|
||||
})
|
||||
|
||||
// --- Korrekturen aus der Prüfung (Review 1) ---
|
||||
|
||||
const v4Section = (fromId, toId) => section(read(V4), fromId, toId)
|
||||
|
||||
test('Widerrufsfrist beginnt mit dem Vertragsschluss, nicht mit der Bestellung', () => {
|
||||
const hinweise = v4Section('widerruf-hinweise', 'avv')
|
||||
assert.ok(hinweise.includes('mit dem Vertragsschluss (§ 2 Abs. 3 bzw. Abs. 5)'))
|
||||
assert.ok(!hinweise.includes('mit der Bestellung'))
|
||||
// § 2 Abs. 3 = Vertragsschluss beim Abo, Abs. 5 = Vertragsschluss bei Wechsel und Zusatzspeicher
|
||||
const html = read(V4)
|
||||
const p2 = html.slice(html.indexOf('id="p2"'), html.indexOf('id="p3"'))
|
||||
// Absaetze auf oberster Ebene stehen mit genau vier Leerzeichen am Zeilenanfang (die Liste a-e in Abs. 2 mit acht)
|
||||
const absaetze = [...p2.matchAll(/^ <li>([\s\S]*?)<\/li>$/gm)].map((m) => strip(m[1]))
|
||||
assert.equal(absaetze.length, 6)
|
||||
assert.ok(absaetze[2].startsWith('Der Vertrag kommt zustande, sobald der Kunde den Vorgang bei Stripe abgeschlossen hat'))
|
||||
assert.ok(absaetze[4].includes('Mit dem Klick auf „Zahlungspflichtig bestellen“ kommt der Vertrag zustande'))
|
||||
assert.ok(hinweise.includes('gehören zu diesen Bedingungen, nicht zur gesetzlichen Widerrufsbelehrung'))
|
||||
assert.ok(hinweise.includes('Mehrbetrag ist, was der Kunde seit dem Wechsel mehr gezahlt hat'))
|
||||
assert.ok(hinweise.includes('Wertersatz verlangen wir dafür nicht (Nr. 4)'))
|
||||
})
|
||||
|
||||
test('Wechsel, Plus und Minus im Portal nur ohne Kündigung und ohne offene Zahlung', () => {
|
||||
const wechsel = v4Section('wechsel', 'zusatzspeicher'), speicher = v4Section('zusatzspeicher', 'p11')
|
||||
assert.ok(wechsel.includes('nicht gekündigt'))
|
||||
assert.ok(speicher.includes('nicht gekündigt'))
|
||||
assert.ok(speicher.includes('„Nur den E-Mail-Zusatzspeicher“'))
|
||||
// Übersicht: kein unbedingtes "jederzeit" mehr bei Wechsel und Zusatzspeicher
|
||||
const uebersicht = strip(read(V4)).split('Inhalt ')[0]
|
||||
assert.ok(!/jederzeit im Kundenportal/.test(uebersicht))
|
||||
assert.ok(!/Jederzeit zum Ende des Abrechnungsmonats reduzierbar/.test(uebersicht))
|
||||
})
|
||||
|
||||
test('Bestellübersichten nennen Laufzeit und Kündigungsmöglichkeit (§ 312j Abs. 2 BGB)', () => {
|
||||
assert.ok(v4Section('p2', 'p3').includes('sowie Laufzeit und Kündigungsmöglichkeit'))
|
||||
assert.ok(v4Section('wechsel', 'zusatzspeicher').includes('die Kündigungsmöglichkeit nach Ablauf der neuen Mindestlaufzeit'))
|
||||
assert.ok(v4Section('zusatzspeicher', 'p11').includes('zum Ende jedes Abrechnungsmonats kündbar'))
|
||||
})
|
||||
|
||||
test('Faire Nutzung der Postfächer, Hetzner benannt, Formulare, AVV-Zusatzleistungen', () => {
|
||||
const leistungen = v4Section('p3', 'p4')
|
||||
assert.ok(leistungen.includes('auch unentgeltlich'))
|
||||
assert.ok(leistungen.includes('Versandgrenzen von Hetzner'))
|
||||
assert.ok(leistungen.includes('(im Folgenden „Hetzner“)'))
|
||||
assert.ok(!/\bder Anbieter\b/.test(leistungen))
|
||||
assert.ok(v4Section('p6', 'p7').includes('für ihn tätige Personen'))
|
||||
const hosting = v4Section('hosting', 'wartung')
|
||||
assert.ok(hosting.includes('sind im Hosting-Abo nicht enthalten; eine Lösung dafür bieten wir auf Anfrage gesondert an'))
|
||||
assert.ok(!hosting.includes('Formulardienst'))
|
||||
const avv = v4Section('avv', 'tom')
|
||||
assert.ok(avv.includes('sowie für gesondert vereinbarte Zusatzleistungen zum Abo, etwa eine Formularlösung,'))
|
||||
assert.ok(avv.includes('sowie gesondert vereinbarter Zusatzleistungen'))
|
||||
assert.ok(avv.includes('zu dem der neue Unterauftragsverarbeiter eingesetzt werden soll'))
|
||||
assert.ok(strip(read(V4)).includes('über „Abo verwalten & Rechnungen“'))
|
||||
})
|
||||
|
||||
test('Jeder Preis in Absätzen und Listenpunkten mit Hinweis auf § 19 UStG', () => {
|
||||
// Die Übersichtstabelle ist ausgenommen (<td>), ihre Zeile "Preise" trägt den Hinweis
|
||||
const elemente = [...read(V4).matchAll(/<(li|p)\b[^>]*>([\s\S]*?)<\/\1>/g)].map((m) => strip(m[2]))
|
||||
const mitPreis = elemente.filter((t) => /\d+(,\d\d)? €/.test(t))
|
||||
assert.ok(mitPreis.length >= 7, `Preisstellen: ${mitPreis.length}`)
|
||||
for (const t of mitPreis) assert.ok(t.includes('§ 19 UStG'), `ohne § 19 UStG: ${t.slice(0, 90)}`)
|
||||
})
|
||||
|
||||
test('Alle internen Links und Querverweise treffen', () => {
|
||||
const html = read(V4)
|
||||
const ids = [...html.matchAll(/ id="([^"]+)"/g)].map((m) => m[1])
|
||||
for (const [, ref] of html.matchAll(/href="#([^"]+)"/g)) assert.equal(ids.filter((i) => i === ref).length, 1, `#${ref}`)
|
||||
// Absätze je Paragraph: <li> der ol.abs auf oberster Ebene; bei § 3 die Überschriften "(M)"
|
||||
const absaetze = {}
|
||||
for (const [, n, body] of html.matchAll(/<h2 id="[^"]+">§ (\d+) [^<]*<\/h2>([\s\S]*?)(?=<h2 |<p class="anlage-kopf)/g)) {
|
||||
const h3 = [...body.matchAll(/<h3>\((\d+)\)/g)].map((x) => Number(x[1]))
|
||||
if (h3.length) { absaetze[n] = Math.max(...h3); continue }
|
||||
const start = body.indexOf('<ol class="abs">')
|
||||
let depth = 0, count = 0
|
||||
if (start >= 0) for (const [, close, tag] of body.slice(start).matchAll(/<(\/?)(ol|ul|li)\b[^>]*>/g)) {
|
||||
if (tag === 'li') { if (!close && depth === 1) count++; continue }
|
||||
depth += close ? -1 : 1
|
||||
if (depth === 0) break
|
||||
}
|
||||
absaetze[n] = count
|
||||
}
|
||||
assert.equal(Object.keys(absaetze).length, 16)
|
||||
const text = strip(html)
|
||||
const refs = [...text.matchAll(/§ (\d+) Abs\. (\d+)/g)]
|
||||
assert.ok(refs.length >= 15, `Querverweise: ${refs.length}`)
|
||||
for (const [ref, n, a] of refs) assert.ok(Number(a) <= (absaetze[n] ?? 0), `${ref}: § ${n} hat ${absaetze[n] ?? 0} Absätze`)
|
||||
// Leistungsbeschreibung A/B Nr. N: nummerierte <h3> der jeweiligen Beschreibung
|
||||
const nummern = (fromId, toId) => [...html.slice(html.indexOf(`id="${fromId}"`), html.indexOf(`id="${toId}"`)).matchAll(/<h3>(\d+)\. /g)].map((x) => Number(x[1]))
|
||||
const maxA = Math.max(...nummern('hosting', 'wartung')), maxB = Math.max(...nummern('wartung', 'widerruf'))
|
||||
for (const [ref, ab, nr] of text.matchAll(/Leistungsbeschreibung ([AB]) Nr\. (\d+)/g)) assert.ok(Number(nr) <= (ab === 'A' ? maxA : maxB), ref)
|
||||
for (const [ref, nr] of v4Section('hosting', 'wartung').matchAll(/\bNr\. (\d+)/g)) assert.ok(Number(nr) <= maxA, `A: ${ref}`)
|
||||
for (const [ref, nr] of v4Section('wartung', 'widerruf').matchAll(/\bNr\. (\d+)/g)) assert.ok(Number(nr) <= maxB, `B: ${ref}`)
|
||||
})
|
||||
Reference in new issue
Block a user