76 lines
8.9 KiB
JavaScript
76 lines
8.9 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import { join } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { spawnSync } from 'node:child_process'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
|
||
// Rechtstexte der Funktionen in Fassung 6 (Entwurf): Leistungsbeschreibungen und AVV ohne Preise (Tasks 7 bis 10).
|
||
|
||
useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' })
|
||
const { render, funktionen6Dokumente } = await import('../scripts/build-legal-html.mjs')
|
||
|
||
const ROOT = fileURLToPath(new URL('..', import.meta.url))
|
||
const lies = (rel) => readFileSync(join(ROOT, rel), 'utf8')
|
||
const ENTWURF = true
|
||
|
||
const B = { // Bausteine; {N} = Nominativ, {G} = Genitiv der Partei (Kunde/Kunden, Händler/Händlers, Vermieter/Vermieters)
|
||
geltung: 'Diese Leistungsbeschreibung gilt, wenn der {N} die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der {N} die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat.',
|
||
voraussetzung: 'Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.',
|
||
einrichtung: 'Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des {G}.',
|
||
projekt: 'Hat WEBklar die Funktion auf Wunsch des {G} schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.',
|
||
mehraufwand: 'Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des {G} in Textform aus; es gilt § 4 der Abo-Bedingungen.',
|
||
widerruf: ['Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.',
|
||
'Hat der {N} die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.'],
|
||
ende: 'Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab.', // alle Funktionen außer Öffnungszeiten live (R8: Tagesjob)
|
||
endeZeiten: 'Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo, läuft die Funktion noch 30 Tage unverändert weiter; danach schaltet WEBklar sie ab.', // nur Öffnungszeiten live: wie das bestehende Zeiten-Modul, P6 (Mail K40) und P4 (Text Z2); kein Behalten gegen Entgelt (R8, mit P4/P6 abgestimmt)
|
||
}
|
||
const PARTEI = { kunde: { N: 'Kunde', G: 'Kunden' }, haendler: { N: 'Händler', G: 'Händlers' }, vermieter: { N: 'Vermieter', G: 'Vermieters' } }
|
||
const bau = (t, p) => (Array.isArray(t) ? t.map((x) => bau(x, p)) : t.replaceAll('{N}', PARTEI[p].N).replaceAll('{G}', PARTEI[p].G))
|
||
// „Haken“ der Gäste im Widget (Terminplaner) und „Widerrufsbelehrung für Gäste“ bleiben erlaubt; verboten sind die Wörter der alten Bestellung
|
||
const VERBOTEN = /€|per Haken|Haken diesem|Zahlungspflichtig|Einrichtungspreis|Gesamtpreis|Bestellung der Einrichtung|im Kundenportal bestellt|Einrichtung von Funktionen|ohne Wartungs-Abo|auch im Hosting-Abo|Hosting-Abo, Wartungs-Abo oder|Vertrag über den Betrieb (der|seiner) Website|solange WEBklar die Website|[a-zäöüß,;:] Sie\b|\bIhr(e[nmrs]?)?\b/ // „Sie“ am Satzanfang ist das Pronoun für die Funktion („Sie setzt ein laufendes Wartungs-Abo voraus“), erlaubt
|
||
const LISTE = ['termine', 'news', 'zeiten'] // Task 8 ergänzt shop, unterkunft; Task 9 analyse
|
||
const LB = { // frist = Satzteil, der die Löschfrist nennt (C12); pflicht = funktionsspezifischer Pflichtsatz
|
||
termine: { slug: 'leistungsbeschreibung-terminplaner', partei: 'kunde', alt: 'docs/rechtliches/leistungsbeschreibung-terminplaner.md', frist: '30 Tage nach dem Abschalten',
|
||
pflicht: 'Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.' },
|
||
news: { slug: 'leistungsbeschreibung-news', partei: 'kunde', frist: 'Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein.',
|
||
pflicht: 'Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.',
|
||
nr: ['1. Geltung', '2. Leistung', '3. Einrichtung', '4. Pflichten des Kunden', '5. Ende der Leistung', '6. Widerruf'] },
|
||
zeiten: { slug: 'leistungsbeschreibung-zeiten', partei: 'kunde', frist: 'Die Eingaben des Kunden bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt schon in den 30 Tagen Weiterlauf wieder ein Wartungs-Abo, läuft die Funktion einfach weiter, beginnt es in den 30 Tagen danach, schaltet WEBklar die Funktion mit den vorhandenen Eingaben ohne weitere Kosten wieder ein.',
|
||
nr: ['1. Geltung', '2. Leistung', '3. Einbau und Freischaltung', '4. Einrichtung', '5. Nicht enthalten', '6. Pflichten des Kunden', '7. Verfügbarkeit und Aktualität', '8. Haftung', '9. Ende der Leistung', '10. Änderungen und Einstellung', '11. Widerruf', '12. Hinweise'] },
|
||
}
|
||
const abschnitte = (md) => md.split(/^## /m).slice(1).map((s) => ({ kopf: s.split('\n')[0], absaetze: s.split(/\n\n/).filter((b) => /^\(\d+\) /.test(b)) }))
|
||
test('Leistungsbeschreibungen v6: Fassungszeile, Bausteine, Pflichtsatz, Frist, keine Preise', () => {
|
||
for (const key of LISTE) {
|
||
const e = LB[key], md = lies(`docs/rechtliches/${e.slug}-v6.md`)
|
||
assert.match(md.split('\n')[2], new RegExp(`^\\*\\*Fassung 2026-10-16\\*\\*${ENTWURF ? ' · Entwurf – Prüfung durch Kanzlei offen' : ''} · Zusatzbedingungen zum Wartungs-Abo$`), key)
|
||
assert.doesNotMatch(md, VERBOTEN, key)
|
||
for (const t of ['geltung', 'voraussetzung', 'einrichtung', 'projekt', 'mehraufwand', key === 'zeiten' ? 'endeZeiten' : 'ende']) assert.ok(md.includes(bau(B[t], e.partei)), `${key}: Baustein ${t}`)
|
||
if (key === 'zeiten') assert.doesNotMatch(md, /Karenz|Behalten|spätestens am folgenden Tag/, 'Zeiten: 30 Tage Weiterlauf wie das bestehende Modul, kein Behalten, nicht die Abschaltung am Folgetag')
|
||
for (const t of bau(B.widerruf, e.partei)) assert.ok(md.includes(t), `${key}: Baustein widerruf`)
|
||
if (e.pflicht) assert.ok(md.includes(e.pflicht), `${key}: Pflichtsatz`)
|
||
assert.ok(md.includes(e.frist), `${key}: Frist`)
|
||
if (e.nr) assert.deepEqual(abschnitte(md).map((a) => a.kopf), e.nr, key)
|
||
}
|
||
})
|
||
test('Terminplaner v6: Überschriften wie Fassung 5 (Nr. 5 heißt „Einrichtung“), Absatzzahlen, auf die der AVV verweist', () => {
|
||
const v5 = abschnitte(lies(LB.termine.alt)), v6 = abschnitte(lies('docs/rechtliches/leistungsbeschreibung-terminplaner-v6.md'))
|
||
assert.deepEqual(v6.map((a) => a.kopf), v5.map((a) => (a.kopf === '5. Einrichtung und Preis' ? '5. Einrichtung' : a.kopf)))
|
||
assert.deepEqual(v6.map((a) => a.absaetze.length), v5.map((a) => (a.kopf.startsWith('10. ') ? 2 : a.absaetze.length)), 'Absatzzahlen wie Fassung 5, Nr. 10 hat die zwei Absätze des Bausteins widerruf')
|
||
assert.ok(v6[7].absaetze[0].includes(bau(B.ende, 'kunde')), 'Nr. 8 Abs. 1 ist das Ende (AVV Terminplaner Nr. 1 Abs. 2)')
|
||
})
|
||
test('Seiten der Gruppe sind genau render(Markdown)', () => {
|
||
for (const key of LISTE) {
|
||
const d = funktionen6Dokumente().find((x) => x.html.includes(`/${LB[key].slug}-2026-10-16.html`))
|
||
assert.equal(lies(d.html), render(lies(d.md), d.meta), `${d.html}: node scripts/build-legal-html.mjs funktionen6 --nur=${d.doc}`)
|
||
}
|
||
})
|
||
test('Entwurfsseiten der Gruppe: Entwurfsmarke in der Quelle und meta robots noindex in der Seite (N45)', () => {
|
||
for (const key of LISTE) {
|
||
const d = funktionen6Dokumente().find((x) => x.html.includes(`/${LB[key].slug}-2026-10-16.html`))
|
||
assert.equal(lies(d.md).includes('Entwurf – Prüfung durch Kanzlei offen'), ENTWURF, d.md)
|
||
assert.equal(lies(d.html).includes('<meta name="robots" content="noindex">'), ENTWURF, d.html)
|
||
}
|
||
})
|