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:
1 parent
5f99f08a39
commit
bea07e73b1
22 files changed
+299
-33
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.**']
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/ },
|
||||
|
||||
Reference in new issue
Block a user