Rechtstexte und K15: Prüfbefunde F5–F8 (AVV genauer, PDF-Prüfsumme, Unterlagen der vereinbarten Fassung)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c059cb3d6
commit
90098726b9
13 files changed
+345
-63
No files matched your search
@@ -32,6 +32,7 @@ const { setMailTransportForTests } = await import('../server/services/mailer.js'
|
||||
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
||||
const { speichereEinstellung, resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
||||
const { halte } = await import('../server/services/termine/ablauf.js')
|
||||
const { loeschBezugsfeld } = await import('../server/services/termine/buchungen.js')
|
||||
after(() => setMailTransportForTests(null))
|
||||
|
||||
// Vertragsunterlagen des Terminplaners als Markdown: Leistungsbeschreibung (Task F4), AVV (Task F5).
|
||||
@@ -450,6 +451,14 @@ test('avv_gleich_datenschutz_absatz: Fristen und Orte stimmen mit dem Datenschut
|
||||
assert.ok(absatz.includes('nicht zustellbare E-Mails nach 48 Stunden'))
|
||||
assert.ok(absatz.includes('Rechenzentren Falkenstein und Nürnberg') && absatz.includes('Eine Übermittlung in Drittländer findet nicht statt.'))
|
||||
assert.ok(absatz.includes('Der Server speichert sie kurzzeitig in Protokollen und löscht sie nach 14 Tagen'))
|
||||
// unbeantwortete Terminanfragen: derselbe Satz im Datenschutz-Absatz des Kunden und im AVV (Nr. 9 Abs. 2), und der Code tut es
|
||||
assert.ok(absatz.includes('Terminanfragen, die wir bis zum Terminbeginn nicht beantworten, behandeln wir als abgelehnt.'))
|
||||
const nr9abs2 = absatzTexte(abschnitt(AVV, 9))[1]
|
||||
assert.ok(nr9abs2.startsWith('Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung.'))
|
||||
assert.ok(nr9abs2.includes('Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.'))
|
||||
assert.ok(JOBS.includes("angefragt mit Beginn in der Vergangenheit => abgelehnt vom System"), 'jobs.js lehnt verstrichene Anfragen ab')
|
||||
assert.equal(loeschBezugsfeld('angefragt'), undefined, 'eine Anfrage wird nie unmittelbar gelöscht, erst als abgelehnte nach 30 Tagen')
|
||||
assert.equal(loeschBezugsfeld('abgelehnt'), 'updatedAt', 'die 30 Tage laufen ab der letzten Änderung, also ab der Ablehnung durch das System')
|
||||
// der AVV nennt dieselben Zahlen
|
||||
const nr3 = avvNr(3)
|
||||
assert.ok(nr3.includes('In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).'))
|
||||
@@ -500,6 +509,12 @@ test('avv_keine_ip_in_buchung: weder das Schema noch eine über halte gehaltene
|
||||
assert.equal(buchungen.length, 1)
|
||||
const nachweis = JSON.parse(buchungen[0].nachweis)
|
||||
assert.ok(nachweis.fassung && nachweis.datenschutz, 'der Nachweis enthält Fassung und Hash des Datenschutz-Hinweises')
|
||||
// genau die Bestandteile, die AVV Nr. 3 Abs. 1 und Nr. 8 Abs. 5 nennen: Fassung, Zeitpunkt, Knopftext, Anrede, Hashes (keine weiteren Klartexte)
|
||||
const erlaubt = new Set(['fassung', 'am', 'knopf', 'anrede', 'datenschutz', 'storno', 'widerruf', 'vorzeitig', 'erinnerung'])
|
||||
assert.deepEqual(Object.keys(nachweis).filter((k) => !erlaubt.has(k)), [])
|
||||
for (const k of ['fassung', 'am', 'knopf', 'anrede']) assert.equal(typeof nachweis[k], 'string', k)
|
||||
assert.deepEqual(Object.keys(nachweis.storno), ['version', 'hash', 'haken', 'am'])
|
||||
assert.deepEqual(Object.keys(nachweis.erinnerung), ['stunden', 'hash', 'am'])
|
||||
const schluessel = (wert) => (wert && typeof wert === 'object' ? Object.entries(wert).flatMap(([k, v]) => [k, ...schluessel(v)]) : [])
|
||||
assert.deepEqual(schluessel(nachweis).filter((k) => nameIp.test(k)), [])
|
||||
// der Nachweis hält Hashes, keine Texte: jeder Hash ist 64 Hexzeichen
|
||||
@@ -516,6 +531,32 @@ test('avv_keine_ip_in_buchung: weder das Schema noch eine über halte gehaltene
|
||||
}
|
||||
})
|
||||
|
||||
/** Jedes Attribut einer Buchung ist in AVV Nr. 3 Abs. 1 genannt (Wortlaut) oder eine technische Kennung/Eigenschaft des Dienstes. */
|
||||
const BUCHUNG_IM_AVV = {
|
||||
gastName: 'Name und E-Mail-Adresse', gastEmail: 'Name und E-Mail-Adresse', gastTelefon: 'Telefonnummer', gastNachricht: 'eine Nachricht', gastZusatz: 'Zusatzfelder',
|
||||
leistungName: 'die gebuchte Leistung', ressourceName: 'die Person oder der Platz', startsAt: 'der Termin', endsAt: 'der Termin', blockEndsAt: 'der Termin',
|
||||
status: 'der Status der Buchung mit den Zeitpunkten', heldUntil: 'der Status der Buchung mit den Zeitpunkten', bestaetigtAt: 'der Status der Buchung mit den Zeitpunkten',
|
||||
angenommenAt: 'der Status der Buchung mit den Zeitpunkten', abgesagtAt: 'der Status der Buchung mit den Zeitpunkten', abgesagtVon: 'der Status der Buchung mit den Zeitpunkten',
|
||||
widerrufenAt: 'der Status der Buchung mit den Zeitpunkten', createdAt: 'der Status der Buchung mit den Zeitpunkten', updatedAt: 'der Status der Buchung mit den Zeitpunkten',
|
||||
absageGrund: 'Absagegrund und Vermerke zu einer Absage oder einem Widerruf', nachweis: 'der Nachweis der Zustimmungen',
|
||||
preisCent: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde', zahlart: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde',
|
||||
zahlStatus: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde', verbraucher: 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde',
|
||||
erinnerung: 'der Wunsch nach einer Erinnerung', erinnerungGesendetAt: 'der Wunsch nach einer Erinnerung',
|
||||
}
|
||||
const BUCHUNG_TECHNISCH = ['featureId', 'customerId', 'projectId', 'leistungId', 'ressourceId', 'preisArt', 'buchungsart', 'quelle', 'verifyHash', 'manageHash', 'stripeSessionId', 'stripePaymentIntent', 'icsSequenz']
|
||||
|
||||
test('avv_datenarten_decken_schema: jedes Attribut einer Buchung steht in Nr. 3 Abs. 1 oder ist eine technische Kennung', () => {
|
||||
const nr3abs1 = norm(absatzTexte(abschnitt(AVV, 3))[0])
|
||||
const attribute = TERMINE_COLLECTIONS.termineBookings.attributes.map((a) => a.key)
|
||||
const unbekannt = attribute.filter((k) => !Object.hasOwn(BUCHUNG_IM_AVV, k) && !BUCHUNG_TECHNISCH.includes(k))
|
||||
assert.deepEqual(unbekannt, [], 'neues Attribut der Buchung: in AVV Nr. 3 Abs. 1 aufnehmen oder als technisch einstufen')
|
||||
for (const [k, wort] of Object.entries(BUCHUNG_IM_AVV)) {
|
||||
assert.ok(attribute.includes(k), `${k} gibt es nicht mehr im Schema`)
|
||||
assert.ok(nr3abs1.includes(wort), `${k}: „${wort}“ fehlt in Nr. 3 Abs. 1`)
|
||||
}
|
||||
for (const k of BUCHUNG_TECHNISCH) assert.ok(attribute.includes(k), `${k} gibt es nicht mehr im Schema`)
|
||||
})
|
||||
|
||||
test('avv_verweise_auf_lb: jedes „Nr. N Abs. M der Leistungsbeschreibung“ und jedes „Nummer N“ im AVV zeigt auf Vorhandenes', () => {
|
||||
const text = norm(AVV_MD)
|
||||
|
||||
@@ -600,7 +641,8 @@ test('avv_inhalt: Abschluss, Zwecke ohne Eigennutz, Datenarten, Weisungen, Daten
|
||||
const nr3 = avvNr(3)
|
||||
for (const wort of [
|
||||
'Name und E-Mail-Adresse', 'Telefonnummer', 'Zusatzfelder', 'die gebuchte Leistung, die Person oder der Platz und der Termin', 'der Status der Buchung mit den Zeitpunkten',
|
||||
'Zeitpunkt und als Hashes (Prüfsummen) der gezeigten Texte', 'der Wunsch nach einer Erinnerung', 'der Zustellstatus der E-Mails an den Gast',
|
||||
'Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte', 'der Wunsch nach einer Erinnerung', 'der Zustellstatus der E-Mails an den Gast',
|
||||
'Absagegrund und Vermerke zu einer Absage oder einem Widerruf', 'auch mit dem Namen aus einer Widerrufserklärung', 'Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde',
|
||||
'Namen oder Bezeichnungen der Personen oder Plätze', 'Arbeitszeiten und Abwesenheiten', 'geheimer Kalender-Link',
|
||||
]) assert.ok(nr3.includes(wort), `Nr. 3 nennt ${wort}`)
|
||||
assert.ok(nr3.includes('Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet.'))
|
||||
@@ -611,12 +653,14 @@ test('avv_inhalt: Abschluss, Zwecke ohne Eigennutz, Datenarten, Weisungen, Daten
|
||||
const nr4 = avvNr(4)
|
||||
assert.ok(nr4.includes('Mit der Bestellung weist der Kunde WEBklar an'))
|
||||
assert.ok(nr4.includes('Sonstige Weisungen erteilt der Kunde in Textform'))
|
||||
assert.ok(nr4.includes('Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.'))
|
||||
|
||||
const nr8 = avvNr(8)
|
||||
assert.ok(nr8.includes('Gastdaten löschen') && nr8.includes('CSV-Datei'))
|
||||
assert.ok(nr8.includes('möglichst innerhalb von 24 Stunden nach Kenntnis'))
|
||||
assert.ok(nr8.includes('bis drei Jahre nach dem Ende des Terminplaners'))
|
||||
assert.ok(nr8.includes('nur den Zeitpunkt und die Hashes der Texte'))
|
||||
assert.ok(nr8.includes('die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte'))
|
||||
assert.doesNotMatch(nr8, /nur den Zeitpunkt/)
|
||||
|
||||
const nr11 = avvNr(11)
|
||||
assert.ok(nr11.includes('Art. 13 DSGVO') && nr11.includes('Datenschutz-Absatz') && nr11.includes('Beschäftigten'))
|
||||
@@ -673,6 +717,9 @@ const TOM_FUNDSTELLEN = {
|
||||
{ datei: 'server/services/termine/schema.js', hat: /Eine Einstellung je Funktion; Dokument-ID = ID des portalFeatures-Dokuments/ },
|
||||
{ datei: 'server/services/termine/schema.js', hat: /str\('featureId', 64\)/ },
|
||||
{ datei: 'server/services/termine/buchungen.js', hat: /Query\.equal\('featureId', featureId\)/ },
|
||||
// die Hintergrundläufe arbeiten über alle Funktionen (Löschlauf ohne Funktionsfilter, Aufräumen der Warteschlange nach Status) und löschen Dokument für Dokument
|
||||
{ datei: 'server/services/termine/buchungen.js', hat: /featureId === null \? \[\] : \[Query\.equal\('featureId', featureId\)\]/ },
|
||||
{ datei: 'server/services/termine/outbox.js', hat: /Query\.equal\('status', \['gesendet', 'fehler', 'verworfen'\]\)/ },
|
||||
],
|
||||
'Löschung': [
|
||||
{ datei: 'server/index.js', hat: /setInterval\(termineTagesJob, 24 \* 60 \* 60 \* 1000\)/ },
|
||||
@@ -680,6 +727,9 @@ const TOM_FUNDSTELLEN = {
|
||||
{ datei: 'server/services/termine/jobs.js', hat: /im Zweifel wird nichts geloescht/ },
|
||||
{ datei: 'server/services/termine/jobs.js', hat: /ABGESCHALTET_BEHALTEN_MS = LOESCHFRIST_TAGE \* TAG_MS/ },
|
||||
{ datei: 'server/services/termine/entwuerfe.js', hat: /export const ENTWURF_TAGE = 90/ },
|
||||
{ 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\)/ },
|
||||
],
|
||||
'Protokolle': [
|
||||
{ datei: 'test/termineJobs.test.js', hat: /kein Gastname und keine Gastadresse im Log/ },
|
||||
@@ -730,6 +780,12 @@ test('avv_tom_fundstellen: jede Zeile der Anlage hat ihre Fundstellen im Code, i
|
||||
assert.ok(zeilen['Datensicherung'].includes('die letzten sieben Tagessicherungen'))
|
||||
assert.ok(zeilen['Rechenzentrum'].includes('ISO/IEC 27001') && zeilen['Rechenzentrum'].includes('Falkenstein und Nürnberg'))
|
||||
assert.ok(zeilen['Löschung'].includes('30 Tage nach dem Abschalten'))
|
||||
assert.ok(zeilen['Löschung'].includes('Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen'))
|
||||
assert.doesNotMatch(zeilen['Löschung'], /Vor jeder Löschung/)
|
||||
assert.ok(zeilen['Trennung'].includes('Das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion.'))
|
||||
assert.ok(zeilen['Trennung'].includes('Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht.'))
|
||||
assert.doesNotMatch(zeilen['Trennung'], /Abfragen erfolgen nur je Funktion/)
|
||||
assert.ok(zeilen['Datenminimierung'].includes('der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte'))
|
||||
assert.ok(zeilen['Speicher im Browser'].includes('keine Cookies'))
|
||||
assert.ok(zeilen['Zugriffskontrolle'].includes('konstanter Zeit'))
|
||||
assert.ok(zeilen['Organisation'].includes('Nummer 8 Abs. 3') && zeilen['Organisation'].includes('Nummer 8 Abs. 5'))
|
||||
|
||||
Reference in new issue
Block a user