Datenschutz: Abschnitt 6c Terminplaner, Kundenansicht und Nachweise bei Funktionsbestellung

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 04:34:16 +00:00
1 parent 6ec8885bfd
commit a629b2519b
5 files changed
+135 -43

No files matched your search

+114 -3
View File
@@ -382,7 +382,7 @@ test('Rechtsrunde: Zusatzspeicher ganz oder teilweise kündbar, auch über „Ve
test('Rechtsrunde Datenschutz: Aufbewahrung nach dem Ende des letzten Abos, Zahl der Pakete, kurzer UptimeRobot-Eintrag', () => {
const html = read(DS), ds = strip(html)
assert.equal(ds.split('drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet').length - 1, 2, 'Abschnitt 7 und Kündigungs-/Widerrufsseite')
assert.equal(ds.split('drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet').length - 1, 3, 'Abschnitt 7 (Bestellungen, Funktionsbestellung) und Kündigungs-/Widerrufsseite')
for (const s of ['drei Jahre nach Vertragsende', 'nach dem Ende Ihres letzten Abos']) assert.ok(!ds.includes(s), `noch da: ${s}`)
// Abschnitt 6 (Kundenkonto) verweist für Abos und Bestellungen auf die Fristen in Abschnitt 7
assert.ok(between(ds, '6. Kundenportal', '7. Abos').includes('Für Ihre Abos und Bestellungen gelten die Fristen in Abschnitt 7.'))
@@ -495,9 +495,120 @@ test('legal_datenschutz_news: Abschnitt 6a und Leistungsbeschreibung (Entwurf)',
test('legal_datenschutz_kundenansicht: Abschnitt 6 nennt Kundenansicht, Sperren, Protokoll und 12 Monate', () => {
const html = read(DS)
assert.ok(!html.includes('Eine Nachricht in Ihrem Namen senden können sie dabei nicht.'))
assert.ok(html.includes('Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen.'))
assert.ok(html.includes('Um Sie zu unterstützen, können unsere Mitarbeiter Ihr Portal so öffnen, wie Sie es sehen, und dabei für Sie News-Beiträge anlegen, ändern oder löschen und, wenn Sie den Terminplaner nutzen, dessen Einstellungen und Termine ansehen und ändern.'))
assert.ok(!html.includes('News-Beiträge anlegen, ändern oder löschen. Bestellungen'), 'der alte Satz endet nicht mehr nach den News')
assert.ok(html.includes('Bestellungen, Zahlungen, Änderungen an Ihrem Passwort sowie Nachrichten oder Anträge in Ihrem Namen sind dabei ausgeschlossen.'))
assert.ok(html.includes('Jedes Öffnen und jede Änderung halten wir mit Mitarbeiter und Zeitpunkt fest und löschen diese Einträge nach 12 Monaten.'))
assert.ok(html.includes('Rechtsgrundlage dafür ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist die Nachvollziehbarkeit von Zugriffen auf Ihr Konto.'))
assert.ok(html.includes('Stand 01.10.2026'))
// Stand: Tag der Änderung (TP2 F12a), nicht vor der Kundenansicht (01.10.2026) und nicht vor dem Datum aus termsVersion
const stand = /Stand (\d{2})\.(\d{2})\.(2026)/.exec(html)
assert.ok(stand, 'Stand JJ.MM.2026 fehlt')
const standTag = `${stand[3]}-${stand[2]}-${stand[1]}`
assert.ok(standTag >= '2026-10-01', `Stand ${stand[0]} liegt vor der Kundenansicht (01.10.2026)`)
const terms = /^(\d{4})-(\d{2})-(\d{2})-v\d+$/.exec(config.abo.termsVersion)
assert.ok(terms)
assert.ok(standTag >= `${terms[1]}-${terms[2]}-${terms[3]}`, `Stand ${stand[0]} liegt vor termsVersion ${config.abo.termsVersion}`)
assert.ok(html.includes('<p class="meta">Stand 05.10.2026</p>'), 'Stand = Tag der Änderung')
})
// ---------------------------------------------------------------- Abschnitt 6c und Funktionsbestellung (TP2 F12a)
/** Der Block von <h2 id="termine"> bis vor <h2>7. in public/datenschutz.html (roh, mit Tags). */
const block6c = (html = read(DS)) => {
const a = html.indexOf('<h2 id="termine">')
assert.ok(a >= 0, '<h2 id="termine"> fehlt')
const b = html.indexOf('<h2>7. ', a)
assert.ok(b > a)
return html.slice(a, b)
}
test('legal_datenschutz_termine: Abschnitt 6c steht zwischen 6b und 7 und nennt Rolle, Orte, Fristen und Rückkehr', () => {
const html = read(DS)
const analyse = html.indexOf('id="analyse"')
const termine = html.indexOf('id="termine"')
const sieben = html.indexOf('<h2>7. ')
assert.ok(analyse > 0 && termine > analyse && sieben > termine, 'Reihenfolge 6b < 6c < 7')
assert.equal(html.split('id="termine"').length - 1, 1)
assert.ok(html.includes('<h2 id="termine">6c. Online-Terminbuchung auf Websites unserer Kunden</h2>'))
const roh = block6c(html)
assert.ok(!roh.includes('<!--'), 'kein Kommentar im Block')
const text = strip(roh)
for (const s of ['Auftragsverarbeiter', 'Falkenstein', 'Nürnberg', '48 Stunden', '30 Tage', '90 Tage', 'sieben Tage', 'nicht dauerhaft']) assert.ok(text.includes(s), `6c: ${s}`)
// Rolle: Auftragsverarbeiter für Gäste, Verantwortliche für Betriebsdaten, Fragebogen-Entwurf und Bestellung
assert.ok(text.includes('ausschließlich als Auftragsverarbeiter nach den Weisungen des jeweiligen Kunden'))
assert.ok(text.includes('Verantwortliche sind wir für die Betriebsdaten des Kunden im Kundenportal, für den Entwurf seines Fragebogens und für seine Bestellung'))
// Entwurf, E-Mail über Hetzner (Nürnberg, Abschnitt 8), Aufruf je Modus, keine IP in der Buchung
assert.ok(text.includes('Nicht bestellte Entwürfe löschen wir 90 Tage nach der letzten Änderung'))
assert.ok(text.includes('(Rechenzentrum Nürnberg, Abschnitt 8)'))
assert.ok(text.includes('mit der Buchung speichern wir die IP-Adresse nicht'))
assert.ok(text.includes('schon beim Laden der Seite') && text.includes('Konfiguration mit den Texten und Leistungen'))
// Ende des Abos (Wortlaut der Vorgabe)
assert.ok(text.includes('Endet das Wartungs-Abo oder schalten wir die Funktion ab, schalten wir den Terminplaner spätestens am folgenden Tag ab und löschen alle Termindaten und Einstellungen 30 Tage danach; beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schalten wir ihn mit den vorhandenen Daten wieder ein.'))
assert.ok(text.includes('bleiben gelöschte Daten noch bis zu sieben Tage erhalten'))
// die Verbote der Nachbartests gelten auch hier
for (const verboten of ['Abschnitt 6 und 7', 'spätestens nach 24 Stunden', 'Endet die Zusammenarbeit, schalten wir', 'Unterauftragsverarbeiter', 'im Rahmen des Wartungs-Abos']) assert.ok(!text.includes(verboten), `noch da: ${verboten}`)
// Nummerierung 7 bis 15 unverändert
const titel = [...html.matchAll(/<h2>(\d+)\. ([^<]*)<\/h2>/g)].map((m) => `${m[1]}. ${m[2]}`)
assert.deepEqual(titel, [
'1. Verantwortlicher', '2. Geltungsbereich', '3. Hosting, Server-Protokolle und Überwachung der Erreichbarkeit', '4. Cookies und Speicher im Browser', '5. Kontaktaufnahme', '6. Kundenportal',
'7. Abos und Zahlung über Stripe', '8. E-Mail', '9. Buchhaltung und Rechnungen', '10. Domains unserer Kunden', '11. Interessenten', '12. Empfänger im Überblick', '13. Ihre Rechte',
'14. Pflicht zur Bereitstellung, keine automatisierten Entscheidungen', '15. Änderungen',
])
})
test('datenschutz_6c_gleich_markdown: der Block in public/datenschutz.html ist die Markdown-Quelle, die Quelle ist ohne Entwurfs- und Prüflisten-Reste', () => {
const md = read('docs/rechtliches/termine-datenschutz-6c.md').replace(/\r/g, '')
const fence = /```html\n([\s\S]*?)\n```/.exec(md)
assert.ok(fence, 'Markdown-Quelle ohne html-Block')
const zeilen = (t) => t.split('\n').map((z) => z.trim()).filter((z) => z !== '')
assert.deepEqual(zeilen(block6c()), zeilen(fence[1]), 'HTML-Block und Markdown-Quelle weichen ab')
assert.equal(md.split('```').length - 1, 2, 'genau ein Codeblock')
for (const rest of ['Einbau in TP2', 'nicht eingebaut', 'nicht veröffentlicht', 'Für die Rechtsprüfung offen', 'Entwurf für', 'anwaltlich prüfen lassen', '<!--']) assert.ok(!md.includes(rest), `noch da: ${rest}`)
assert.ok(md.startsWith('# Datenschutzerklärung von WEBklar: Abschnitt 6c'))
assert.ok(md.includes('Auftragsverarbeiter') && md.includes('6c. Online-Terminbuchung'))
})
test('legal_datenschutz_funktionsbestellung: Abschnitt 7 nennt den Nachweis der Funktionsbestellung, die Rechtsgrundlagen, die Löschfrist und die Entwurfsdaten', () => {
const abos = between(strip(read(DS)), '7. Abos und Zahlung über Stripe', '8. E-Mail')
const absatz = 'Bestellen Sie eine Funktion (News-Pinnwand, Terminplaner), speichern wir als Nachweis die Texte und Fassungen, denen Sie zugestimmt haben, den Zeitpunkt Ihres Klicks auf „Zahlungspflichtig bestellen“, die Stripe-Sitzung, IP-Adresse und Browserkennung sowie Ihr Konto und die angemeldete Person. '
+ 'Rechtsgrundlagen sind Art. 6 Abs. 1 lit. b, c und f DSGVO; unser berechtigtes Interesse ist der Nachweis des Vertragsschlusses und der Zustimmungen, insbesondere für den Widerruf. '
+ 'Diese Angaben löschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet. '
+ 'Den Entwurf Ihres Fragebogens speichern wir bis zu 90 Tage nach Ihrer letzten Änderung und löschen ihn danach, wenn Sie nichts bestellt haben.'
assert.ok(abos.includes(absatz), 'Absatz zur Funktionsbestellung (wörtlich) fehlt')
assert.equal(abos.split('Bestellen Sie eine Funktion').length - 1, 1)
// der Absatz steht im HTML als eigener Absatz von Abschnitt 7
assert.ok(read(DS).includes(`<p>${absatz}</p>`))
// 6c nennt die Entwurfsdaten und die Frist ebenfalls (Spec 3.7)
assert.ok(strip(block6c()).includes('Entwurf seines Fragebogens'))
})
test('wiederkehr_satz_gleich_lb: Rückkehr des Abos innerhalb von 30 Tagen steht in 6c, Leistungsbeschreibung, AVV und K22 mit „30 Tage“ und „wieder ein“', async () => {
const { readdirSync } = await import('node:fs')
const k22 = readdirSync(new URL('../server/mail-templates/', import.meta.url)).filter((f) => /^K22 .*\.html$/.test(f))
assert.equal(k22.length, 1, 'genau eine Vorlage K22')
const quellen = {
'6c': block6c(),
Leistungsbeschreibung: read('docs/rechtliches/leistungsbeschreibung-terminplaner.md'),
AVV: read('docs/rechtliches/avv-terminplaner.md'),
K22: read(`server/mail-templates/${k22[0]}`),
}
for (const [name, quelle] of Object.entries(quellen)) {
// Absatz = Markdown-Absatz, <p> oder Tabellenzelle der Mail
const absaetze = quelle.replace(/\r/g, '').split(/\n\s*\n|<\/p>|<\/td>/).map(strip)
const treffer = absaetze.filter((a) => a.includes('wieder ein') && a.includes('Wartungs-Abo'))
assert.equal(treffer.length, 1, `${name}: genau ein Absatz zur Rückkehr`)
assert.ok(treffer[0].includes('30 Tage'), `${name}: Rückkehr ohne 30 Tage`)
// die Rückkehr steht im Zusammenhang mit der Löschung nach 30 Tagen (nicht nur irgendwo im Absatz)
const i = treffer[0].indexOf('wieder ein')
assert.ok(treffer[0].lastIndexOf('30 Tage', i) >= 0, `${name}: die 30 Tage stehen vor der Rückkehr`)
}
})
test('datenschutz_pdf: das PDF der Datenschutzerklärung ist eingecheckt (> 20 KB, %PDF) und das Skript baut es im Modus datenschutz', () => {
const pdf = readFileSync(file('docs/WEBklar-Datenschutzerklaerung.pdf'))
assert.ok(pdf.length > 20 * 1024, `PDF zu klein: ${pdf.length} Bytes`)
assert.equal(pdf.subarray(0, 5).toString('latin1'), '%PDF-')
assert.ok(pdf.subarray(-2048).toString('latin1').includes('%%EOF'), 'PDF vollständig geschrieben')
const skript = read('scripts/build-legal-pdfs.sh')
assert.match(skript, /datenschutz\)\n\s*pdf public\/datenschutz\.html docs\/WEBklar-Datenschutzerklaerung\.pdf\n\s*;;/)
})