TP2 Abschlussprüfung: Rechtstexte (AVV, 6c, News-LB-Archiv, Livegang-Checkliste)

- AVV Vorbemerkung (2): K16 der Textform-Aktivierung trägt nur Leistungsbeschreibung und AVV als PDF
- AVV Nr. 9 Abs. 6, Anlage (Löschung), LB Nr. 8 Abs. 3 und Datenschutz 6c: das Einrichtungs-Ticket bleibt mit einer Kurzfassung der Einstellungen als Arbeitsnachweis (keine Daten von Gästen)
- AVV Nr. 8 Abs. 2: "Gastdaten löschen" löscht nur wartende E-Mails; fehlgeschlagene behalten Adresse und Inhalt bis Nr. 9 Abs. 4
- Datenschutz § 7: Frist für Bestellungen ohne Abo (drei Jahre nach Ablauf des Jahres der Bestellung)
- News-Leistungsbeschreibung vom 30.09.2026 unverändert archiviert (SHA-256-Pin), die geltende Datei verweist darauf
- Kanzlei-Paket: Zitate, Q4/Q9/Q23, Livegang-Checkliste (Abschnitt 7.6/7.7), K23-Hinweis
- build-legal-html: Rückwärtsschrägstrich in Navigationsadressen abgelehnt; TP1-Spezifikation E2 (ohne USt)
- HTML, PDFs und Prüfsummen neu erzeugt (Terminplaner-Unterlagen und Datenschutzerklärung)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 06:54:55 +00:00
1 parent 5f99f08a39
commit bea07e73b1
22 files changed
+299 -33

No files matched your search

+1 -1
View File
@@ -215,7 +215,7 @@ test('generator_lehnt_unbekannte_syntax_ab: Codeblock, Zitat, Bild, Link, rohes
assert.throws(() => render('# Titel\n\n**Fassung 2026-10-06**\n', META), /Fassung 2026-10-05/)
// Meta: Fassung im Format, Adressen der Navigation nur relativ oder https
assert.throws(() => render(`${KOPF}Text\n`, { ...META, fassung: '5.10.2026' }), /fassung/)
for (const href of ['javascript:alert(1)', '//evil.example/x', 'http://example.com', '/x"onmouseover="y', 'data:text/html,x']) {
for (const href of ['javascript:alert(1)', '//evil.example/x', 'http://example.com', '/x"onmouseover="y', 'data:text/html,x', '/\\evil.example/x', '/x\\y', 'https://example.com\\@evil.example/']) {
assert.throws(() => render(`${KOPF}Text\n`, { ...META, nav: [{ href, text: 'x' }] }), /Adresse in der Navigation/, href)
}
assert.throws(() => parse(42), TypeError)
+38
View File
@@ -2,6 +2,8 @@ import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { spawnSync } from 'node:child_process'
import { fileURLToPath } from 'node:url'
const { config } = await import('../server/config.js')
@@ -446,6 +448,39 @@ test('Fassung 4 archiviert: HTML bis auf den PDF-Link bytegleich, PDF bytegleich
assert.ok(archived.includes('Version 4 · Stand 30.09.2026'))
})
// = `git show 3a0b008:public/vertragsunterlagen/funktionen-leistungsbeschreibung.html | sha256sum` (News-Leistungsbeschreibung, Stand 30.09.2026).
// Bestellungen seit dem 30.09.2026 haben diesem Text zugestimmt; die geltende Datei (Stand 05.10.2026) ersetzt ihn nur für künftige Bestellungen.
const NEWS_LB = 'public/vertragsunterlagen/funktionen-leistungsbeschreibung.html'
const NEWS_LB_ARCHIV = 'public/vertragsunterlagen/funktionen-leistungsbeschreibung-2026-09-30.html'
const NEWS_LB_ARCHIV_SHA256 = '88a34b43362f0e403c5d0d64c2503d34de47860cacdab498f3cd72cee6df5bbc'
test('News-Leistungsbeschreibung vom 30.09.2026 unverändert archiviert; die geltende Datei verweist darauf', () => {
assert.equal(sha256(NEWS_LB_ARCHIV), NEWS_LB_ARCHIV_SHA256, 'das Archiv darf nie verändert werden (Bestellungen seit 2026-09-30 beruhen darauf)')
const alt = read(NEWS_LB_ARCHIV)
assert.ok(alt.includes('<p class="meta">Stand 30.09.2026</p>'))
assert.ok(alt.includes('§ 356 Abs. 4 BGB') && alt.includes('30,00 € einmalig, inklusive Umsatzsteuer'), 'der alte Wortlaut, nicht der berichtigte')
assert.ok(!alt.includes('Frühere Fassung'), 'das Archiv trägt keinen Verweis (byte-identisch mit dem Stand 3a0b008)')
// die geltende Datei bleibt die berichtigte Fassung und nennt die frühere mit Link auf das Archiv
const neu = read(NEWS_LB)
assert.notEqual(sha256(NEWS_LB), NEWS_LB_ARCHIV_SHA256)
assert.ok(neu.includes('<p class="meta">Stand 05.10.2026</p>'))
assert.ok(neu.includes('<p class="meta">Frühere Fassung vom 30.09.2026: <a href="/vertragsunterlagen/funktionen-leistungsbeschreibung-2026-09-30.html">Leistungsbeschreibung (Stand 30.09.2026)</a></p>'))
assert.equal(neu.split('funktionen-leistungsbeschreibung-2026-09-30.html').length - 1, 1, 'genau ein Verweis')
// Rest der geltenden Datei unverändert gegenüber der Fassung ohne die neue Zeile: nur diese eine Zeile kam hinzu
const ohneZeile = neu.split('\n').filter((z) => !z.includes('Frühere Fassung vom 30.09.2026')).join('\n')
assert.ok(ohneZeile.includes('Der Einbau kostet 30,00 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG).') && !ohneZeile.includes('§ 356 Abs. 4'))
})
test('News-Leistungsbeschreibung vom 30.09.2026: das Archiv ist bytegleich mit dem Stand 3a0b008 (nur mit Git-Verlauf)', (t) => {
const r = spawnSync('git', ['show', '3a0b008:public/vertragsunterlagen/funktionen-leistungsbeschreibung.html'], {
cwd: fileURLToPath(new URL('..', import.meta.url)),
timeout: 20000,
maxBuffer: 1 << 20,
})
if (r.status !== 0) return t.skip('Git-Verlauf mit 3a0b008 nicht vorhanden')
assert.ok(Buffer.compare(r.stdout, readFileSync(file(NEWS_LB_ARCHIV))) === 0, 'das Archiv weicht vom Stand 3a0b008 ab')
})
test('Fassung 5: Unterstützung am Ende von § 4 Abs. 4, Abs. 7 verweist auf Satz 1, 2 und 4', () => {
const text = strip(read(CUR))
// "Dazu zählen" bleibt direkt hinter Satz 1 und bezieht sich weiter auf die aufgewendete Zeit
@@ -544,6 +579,8 @@ test('legal_datenschutz_termine: Abschnitt 6c steht zwischen 6b und 7 und nennt
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.'))
// Ausnahme: das Einrichtungs-Ticket (Kurzfassung der Einstellungen, Arbeitsnachweis) bleibt, ohne Daten von Gästen (AVV Nr. 9 Abs. 6)
assert.ok(text.includes('wieder ein. Eine Ausnahme ist das Einrichtungs-Ticket, in dem wir die Einrichtung als Arbeitsnachweis festhalten (Abschnitt 6): Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen. In unseren Datensicherungen'))
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}`)
@@ -573,6 +610,7 @@ test('legal_datenschutz_funktionsbestellung: Abschnitt 7 nennt den Nachweis der
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. '
+ 'Bestellen Sie ohne Abo (zum Beispiel nur die News-Pinnwand), löschen wir diese Angaben spätestens drei Jahre nach Ablauf des Jahres der Bestellung. '
+ '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)
+73
View File
@@ -505,6 +505,79 @@ test('paket_konsistenz_abo_ende: LB Nr. 8, AVV Nr. 9, 6c und K22 nennen 30 Tage
assert.ok(drei.includes('30 Tage') && drei.includes('Rückkehr') && drei.includes('spätestens am folgenden Tag'))
})
test('paket_checkliste_livegang: Abschnitt 7.6 nennt die Schritte vor, bei und nach dem Deploy in fester Reihenfolge; Skripte, Indizes und Fristen stimmen mit dem Code überein', () => {
const sieben = abschnitt(7)
const [, ab76] = sieben.split('### 7.6 ')
assert.ok(ab76, 'Abschnitt 7.6')
const [livegang, nachher] = ab76.split('### 7.7 ')
assert.ok(nachher, 'Abschnitt 7.7')
const punkte = (text) => text.split('\n').filter((z) => z.startsWith('- [ ] '))
const l = punkte(livegang)
const folge = [
'scripts/termine-setup.mjs', // (a) Schema des Terminplaners vor dem Code-Deploy
'scripts/feature-setup.mjs', // (b) Schema der Funktionen vor dem Code-Deploy
'scripts/build-legal-pdfs.sh datenschutz', // (c) nur das PDF der Datenschutzerklärung
'ruhigen Zeit', // (d) Deploy bei wenig Verkehr
'Uploads in WOMS', // Uploads nach dem Deploy
'scripts/feature-consent-felder-leeren.mjs', // (f) mit Freigabe des Inhabers
'TERMINE_ENABLED=on', // (e) als allerletzter Schritt
]
assert.equal(l.length, folge.length, 'sieben Schritte')
folge.forEach((wort, i) => assert.ok(l[i].includes(wort), `Schritt ${i + 1} nennt „${wort}“: ${l[i].slice(0, 80)}`))
assert.ok(l[0].includes('--check') && l[1].includes('--check'), 'beide Schema-Skripte erst mit --check')
assert.ok(l[1].includes('Code-Deploy, nicht erst der erste Löschlauf'), '(b) Zeitpunkt: vor dem Code-Deploy')
assert.ok(!PAKET.includes('vor dem ersten Löschlauf der Aufbewahrungsfristen ausführen'), 'der alte Zeitpunkt steht nirgends mehr')
assert.ok(l[2].includes('nicht mit `all`'), '(c) nicht all')
assert.ok(l[3].includes('Fassung der Gästetexte, die der Server beim Speichern hat'), '(d) der Nachweis speichert die Fassung des Servers')
assert.ok(l[5].includes('nur mit Freigabe des Inhabers') && l[5].includes('Probelauf') && l[5].includes('--dry-run') && l[5].includes('--apply'), '(f) Probelauf, dann --apply, mit Freigabe')
assert.ok(l[6].startsWith('- [ ] Als allerletzten Schritt `TERMINE_ENABLED=on`'), '(e) TERMINE_ENABLED=on zuletzt')
assert.ok(sieben.indexOf('TERMINE_ENABLED=on') > sieben.indexOf('scripts/feature-consent-felder-leeren.mjs'), 'on nach allen anderen Schritten')
assert.equal(sieben.split('TERMINE_ENABLED=on').length - 1, 1, 'on wird nur an dieser Stelle gesetzt')
// (a) die genannten Spalten und Indizes stehen im Schema; (b)/(f) die Skripte gibt es und sagen dasselbe wie die Checkliste
const schema = lies('server/services/termine/schema.js')
assert.match(schema, /str\('fragebogen', 20000\)/)
for (const index of ['idx_status_ends', 'idx_status_updated', 'idx_booking']) {
assert.ok(schema.includes(`'${index}'`), `${index} im Schema`)
assert.ok(l[0].includes(`\`${index}\``), `Schritt 1 nennt ${index}`)
}
assert.ok(l[0].includes('`fragebogen`') && l[0].includes('`termineConfig`') && l[0].includes('`termineBookings`') && l[0].includes('`termineOutbox`'))
assert.match(lies('scripts/feature-setup.mjs'), /Vor dem Code-Deploy ausfuehren/)
assert.match(lies('scripts/termine-setup.mjs'), /--check/)
const pdfs = lies('scripts/build-legal-pdfs.sh')
assert.ok(pdfs.includes('\ndatenschutz)\n') && pdfs.includes('\nall)\n'), 'build-legal-pdfs.sh kennt datenschutz und all')
const leeren = lies('scripts/feature-consent-felder-leeren.mjs')
assert.ok(leeren.includes('--dry-run') && leeren.includes('--apply') && leeren.includes('Standard ist der Probelauf'))
// (g) Kunden ohne Abo: der Löschlauf erfasst sie heute nicht, die Datenschutzerklärung nennt die Frist; die erste Frist läuft Ende 2029 ab
const g = punkte(nachher)
assert.equal(g.length, 1)
assert.ok(g[0].includes('31.12.2029') && g[0].includes('`findDueDeletions`') && g[0].includes('ohne Abo'))
assert.match(lies('server/services/retention.js'), /Kunden ohne Abo, zum Beispiel nur mit News, sind nie faellig/, 'steht das nicht mehr im Code, ist die Löschung umgesetzt: Punkt 7.7 und Q23 anpassen')
assert.ok(mailText('public/datenschutz.html').includes('Bestellen Sie ohne Abo (zum Beispiel nur die News-Pinnwand), löschen wir diese Angaben spätestens drei Jahre nach Ablauf des Jahres der Bestellung.'))
assert.ok(abschnitt(4).includes('Für Bestellungen ohne Abo (nur News) nennt die Erklärung eine eigene Frist') && abschnitt(4).includes('(31.12.2029)'))
// (h) K23 verspricht „unverändert da“; der Löschlauf löscht auch bei abgeschaltetem Terminplaner Termine nach ihren Fristen
const vier = punkte(abschnitt(7).split('### 7.4 ')[1].split('### 7.5 ')[0])
assert.ok(vier.some((z) => z.includes('K23') && z.includes('unverändert da') && z.includes('abschwächen')), '(h) K23 in 7.4')
assert.ok(mailText('server/mail-templates/K23 Terminplaner laeuft wieder.html').includes('Deine Termine und Einstellungen sind unverändert da'))
assert.match(lies('server/services/termine/jobs.js'), /portalFeatures featureKey termine, active oder disabled/)
// 7.5 verweist auf 7.6
assert.ok(abschnitt(7).split('### 7.5 ')[1].split('### 7.6 ')[0].includes('Abschnitt 7.6'))
})
test('paket_ausnahme_einrichtungs_ticket_und_archiv: Q4, Q9 und Abschnitt 2.3 nennen das archivierte News-Dokument und die Ausnahme der Löschung', () => {
const alle = abschnitt(4)
const q4 = alle.split('### Q4: ')[1].split('### Q5: ')[0]
assert.ok(q4.includes('`public/vertragsunterlagen/funktionen-leistungsbeschreibung-2026-09-30.html`'))
assert.ok(existsSync(new URL('../public/vertragsunterlagen/funktionen-leistungsbeschreibung-2026-09-30.html', import.meta.url)))
assert.ok(q4.includes('unveränderte Kopie des Standes 3a0b008') && q4.includes('Prüfsumme'))
const q9 = alle.split('### Q9: ')[1].split('### Q10: ')[0]
assert.ok(q9.includes('(g) Das Einrichtungs-Ticket') && q9.includes('Leistungsbeschreibung Nr. 8 Abs. 3 und AVV Nr. 9 Abs. 6'))
assert.ok(abschnitt(2).split('### 2.3 ')[1].split('### 2.4 ')[0].includes('das Einrichtungs-Ticket des Teams bleibt als Arbeitsnachweis'))
})
// ------------------------------------------------------------------------------------------------ Fragen Q1 bis Q26 (Task F15b)
const FRAGE_TEILE = ['**Frage.**', '**Fundstelle im Text.**', '**Unsere Annahme.**', '**Alternative.**']
+23 -1
View File
@@ -10,7 +10,7 @@ import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
// stehen Kopfzeilen, der Hinweis und der Verweis im Ticket. websiteProjects, workorders und customers legt das Setup-Skript nicht
// an: ohne Schema im Nachbau.
const aw = await startAppwriteMock({
schema: { portalFeatures: schemaKeys('portalFeatures'), termineConfig: schemaKeys('termineConfig') },
schema: { portalFeatures: schemaKeys('portalFeatures'), termineConfig: schemaKeys('termineConfig'), termineBookings: schemaKeys('termineBookings') },
datetimes: TERMINE_DATETIMES,
})
after(() => aw.close())
@@ -191,6 +191,28 @@ test('termineTicketDetails: Namen mit Markup bleiben Text, halbe Zeichenpaare we
assert.ok((await termineTicketDetails(ctxFuer(doc, projekt, null))).includes('Kunde: c1'))
})
test('Ticket als bleibender Arbeitsnachweis (AVV Nr. 9 Abs. 6): Betriebsangaben, Leistungsnamen, nur die ersten acht Personen, Wünsche des Kunden - und nichts von den Gästen', async () => {
const e = einstellung()
e.ressourcen = Array.from({ length: 10 }, (_, i) => ({
id: `r_tk${String(i).padStart(5, '0')}`, typ: 'person', name: `Mitarbeiter ${i + 1}`, aktiv: true, plan: { 1: [['09:00', '12:00']] }, feedToken: neuerFeedToken(),
}))
const fb = { einbau: { modus: 'inline', knopfText: '', stelle: 'Startseite', gestaltung: '', bemerkung: 'Bitte vor dem 20.10.' } }
const { doc } = await bestelle({ e, fb })
// eine Buchung dieses Terminplaners mit allen Angaben des Gastes: nichts davon darf ins Ticket gelangen
aw.seed('termineBookings', {
$id: 'b_tk1', featureId: doc.$id, customerId: 'c1', status: 'bestaetigt', leistungName: e.leistungen[0].name, ressourceName: 'Mitarbeiter 1',
gastName: 'Erika Gastgeberin', gastEmail: 'erika.gast@example.test', gastTelefon: '0171 5550000', gastNachricht: 'Bitte ohne Föhn', gastZusatz: '{"allergie":"Nuss"}', absageGrund: 'Krank',
})
const text = await termineTicketDetails(ctxFuer(doc))
assert.ok(text.includes('Projekt: Salon Haarlinie') && text.includes('Kunde: Kunde Eins'), 'Betriebsangaben')
assert.ok(text.includes(BETRIEB.telefon), 'Telefon des Betriebs (Ausweichtext im Einbau-Code)')
for (const l of e.leistungen) assert.ok(text.includes(l.name), `Leistung ${l.name}`)
for (let i = 1; i <= 8; i += 1) assert.ok(text.includes(`Mitarbeiter ${i}`), `Mitarbeiter ${i}`)
assert.ok(!text.includes('Mitarbeiter 9') && !text.includes('Mitarbeiter 10') && text.includes('und 2 weitere'), 'nur die ersten acht Namen')
assert.ok(text.includes('Bemerkung: Bitte vor dem 20.10.') && text.includes('Stelle: Startseite'), 'Wünsche des Kunden zum Einbau')
for (const gast of ['Erika', 'Gastgeberin', 'erika.gast', '0171', 'Föhn', 'Nuss', 'Krank']) assert.ok(!text.includes(gast), `keine Gastdaten: ${gast}`)
})
test('termineTicketDetails: Gesundheit (Physiotherapie) und Maximalbefuellung: enthaelt Physio und bleibt hoechstens 7000 Zeichen', async () => {
const e = maximalEinstellung()
const meldungen = gesundheitsMeldungen(e)
+38 -1
View File
@@ -625,7 +625,10 @@ test('avv_inhalt: Abschluss, Zwecke ohne Eigennutz, Datenarten, Weisungen, Daten
assert.ok(vorbemerkung.includes('Das Portal speichert die Fassung, den Zeitpunkt, das Kundenkonto und die angemeldete Person.'))
assert.ok(vorbemerkung.includes('Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte und die Widerrufsbelehrung in dieser Fassung als PDF enthält'))
assert.ok(vorbemerkung.includes('Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform'))
assert.ok(vorbemerkung.includes('Der Kunde erhält die E-Mail nach Absatz 1 anschließend.'))
// T1: In der Textform-Aktivierung trägt K16 nur Leistungsbeschreibung und AVV als PDF (ohne Widerrufsbelehrung, ohne Links): der Text verspricht nicht die E-Mail nach Absatz 1
assert.ok(vorbemerkung.includes('Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.'))
assert.ok(!vorbemerkung.includes('E-Mail nach Absatz 1 anschließend'), 'der Satz verwies auf die Mail mit Belehrung und Links')
assert.match(lies('server/routes/admin/funktionenAdmin.js'), /featureLegalAttachments\('termine', \{ ohne: \['widerruf'\] \}\)/, 'K16 der Textform-Aktivierung: nur Leistungsbeschreibung und AVV')
assert.ok(vorbemerkung.includes('Für den Terminplaner geht er Anlage 1 vor'))
assert.ok(vorbemerkung.includes('Anlage 1 der Abo-Bedingungen'))
@@ -673,6 +676,37 @@ test('avv_inhalt: Abschluss, Zwecke ohne Eigennutz, Datenarten, Weisungen, Daten
assert.ok(nr12.includes('Im Übrigen gilt § 16 der Abo-Bedingungen.'))
})
test('avv_ausnahmen_loeschung: Einrichtungs-Ticket bleibt (Nr. 9 Abs. 6, Anlage, LB Nr. 8 Abs. 3); fehlgeschlagene E-Mails behalten Adresse und Inhalt bis Nr. 9 Abs. 4 (Nr. 8 Abs. 2)', () => {
// T2: das Einrichtungs-Ticket (workorders.details = Kurzfassung der Einstellungen, ticketDetails.js) wird beim Löschlauf nicht angefasst
const ticket = 'Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen'
const nr9 = avvNr(9)
const abs6 = nr9.slice(nr9.indexOf('(6) '), nr9.indexOf('(7) '))
assert.ok(abs6.includes(ticket))
assert.ok(abs6.includes('(zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6).'))
// Reihenfolge: erst die Löschung nach 30 Tagen, dann die Ausnahme, dann der Export; „Bis dahin“ hätte sich auf die Datenschutzerklärung beziehen lassen
assert.ok(abs6.indexOf('30 Tage danach.') < abs6.indexOf(ticket) && abs6.indexOf(ticket) < abs6.indexOf('Bis zur Löschung kann der Kunde die Termine'))
assert.ok(!abs6.includes('Bis dahin'))
assert.ok(tomZeilen()['Löschung'].includes('das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6)'))
assert.ok(norm(abschnitt(LB, 8).text).includes('Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).'))
// die Zahl „acht“ ist die der Kurzfassung (einbauanleitung.js KURZ_NAMEN); die Kurzfassung hat keinen Zugriff auf Buchungen (ticketDetails.js liest nur den Stand der Einstellungen)
assert.match(lies('server/services/termine/einbauanleitung.js'), /const KURZ_NAMEN = 8\b/)
assert.doesNotMatch(lies('server/services/termine/ticketDetails.js'), /termineBookings|buchungen\.js|gastName|gastEmail/)
// Datenschutzerklärung Abschnitt 6 nennt die Arbeitsnachweise und die Aufbewahrung bis zur Verjährung
const ds = norm(lies('public/datenschutz.html').replace(/<[^>]+>/g, ' '))
assert.ok(ds.includes('unsere Arbeitsnachweise') && ds.includes('bis zum Ablauf der Verjährungsfrist (in der Regel drei Jahre)'))
// T3: „Gastdaten löschen“ löscht nur Einträge im Status wartet (verwerfeWartende); Einträge im Status fehler behalten Adresse und Inhalt bis 48 Stunden nach dem Fehlschlag
const nr8 = avvNr(8)
assert.ok(nr8.includes('Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig.'))
assert.ok(nr8.includes('E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag).'))
assert.ok(!nr8.includes('Noch nicht versandte E-Mails zu dieser Buchung löscht WEBklar dabei'), 'der alte Satz versprach mehr, als verwerfeWartende löscht')
const verwerfe = /export async function verwerfeWartende[\s\S]*?\n\}\n/.exec(OUTBOX)?.[0]
assert.ok(verwerfe, 'verwerfeWartende')
assert.ok(verwerfe.includes("Query.equal('status', 'wartet')") && !verwerfe.includes("'fehler'"), 'verwerfeWartende löscht nur den Status wartet')
assert.match(OUTBOX, /const FEHLER_BEHALTEN_MS = 48 \* STUNDE_MS/)
assert.ok(avvNr(9).includes('löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag'))
})
/**
* Fundstellen der Anlage TOM: jede Zeile der Tabelle nennt hier, wo im Code, in den Tests oder in den Abo-Bedingungen ihre
* Aussage gesichert ist. { datei, hat } = die Datei enthält das Muster; { datei, nicht } = die Datei enthält es nicht.
@@ -730,6 +764,9 @@ const TOM_FUNDSTELLEN = {
{ datei: 'server/services/termine/entwuerfe.js', hat: /const funktion = await holeOderNull\(config\.collections\.portalFeatures, id\)/ },
// die Warteschlange löscht die gelisteten Einträge ohne erneutes Lesen (nur Buchungen, abgeschaltete Funktionen und Entwürfe lesen frisch)
{ datei: 'server/services/termine/outbox.js', hat: /await deleteDocument\(col\(\), eintrag\.\$id\)/ },
// das Einrichtungs-Ticket (Kurzfassung der Einstellungen, ohne Daten von Gästen) bleibt als Arbeitsnachweis
{ datei: 'server/services/termine/ticketDetails.js', hat: /const \{ kurz \} = einbauanleitung\(/ },
{ datei: 'server/services/termine/einbauanleitung.js', hat: /const KURZ_NAMEN = 8 / },
],
'Protokolle': [
{ datei: 'test/termineJobs.test.js', hat: /kein Gastname und keine Gastadresse im Log/ },