import test from 'node:test' import assert from 'node:assert/strict' import { loadModule } from './load.mjs' // Kunden-Reiter "Funktionen": Register der Funktionen (News, Terminplaner), Aktionen je Zustand, Antwort einarbeiten const { FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText, hindernisText, anleitungKopien, anleitungPfad, panelModell, anleitungOffen, textformNoetig, termineTextformLabel, termineChatTemplate, textformAnsicht, textformBereit, featureBody, } = await loadModule('src/lib/funktionenView.js') test('FUNKTIONEN: News und Terminplaner mit den Bestätigungstexten', () => { assert.deepEqual(FUNKTIONEN, [ { key: 'news', label: 'News', bestaetigen: { activate: 'News ohne Zahlung aktivieren?', disable: 'News abschalten? Die Website zeigt dann keine Beiträge mehr.', }, }, { key: 'termine', label: 'Terminplaner', bestaetigen: { activate: 'Terminplaner ohne Zahlung aktivieren?', disable: 'Terminplaner abschalten? Auf der Website sind dann keine Buchungen mehr möglich. Damit beginnt die Löschung: Nach 30 Tagen werden alle Termine und Einstellungen gelöscht. Wieder einschalten ist nach diesen 30 Tagen nur mit einer neuen Bestellung (149 €) möglich.', abnahme: 'Terminplaner freischalten? Der Kunde bekommt die Mail „bereit“ und Gäste können buchen.', ticket: 'Fehlendes Einrichtungs-Ticket neu anlegen?', }, }, ]) }) test('W1: der Abschalten-Dialog des Terminplaners nennt Buchungsstopp, Löschung nach 30 Tagen und die neue Bestellung (149 €)', () => { const text = FUNKTIONEN.find((f) => f.key === 'termine').bestaetigen.disable assert.match(text, /^Terminplaner abschalten\?/) assert.match(text, /keine Buchungen mehr möglich/, 'Buchungen stehen') assert.match(text, /Nach 30 Tagen werden alle Termine und Einstellungen gelöscht/, 'Löschung nach 30 Tagen') assert.match(text, /nur mit einer neuen Bestellung \(149 €\)/, 'danach nur mit neuer Bestellung, mit Preis') // News bleibt unverändert: dort wird nichts gelöscht assert.equal(FUNKTIONEN.find((f) => f.key === 'news').bestaetigen.disable, 'News abschalten? Die Website zeigt dann keine Beiträge mehr.') }) test('ZUSTAND: Ton und Beschriftung je Zustand', () => { assert.deepEqual(ZUSTAND, { off: ['muted', 'Aus'], ordered: ['warn', 'Bestellt'], setup: ['info', 'Einrichtung'], active: ['ok', 'Aktiv'], disabled: ['muted', 'Abgeschaltet'], }) }) test('funktionOf: Ansicht der Funktion, ohne Eintrag "off"', () => { const project = { projectId: 'p1', features: { news: { state: 'active', ticketWoid: '100' }, termine: { state: 'setup' } } } assert.deepEqual(funktionOf(project, 'news'), { state: 'active', ticketWoid: '100' }) assert.deepEqual(funktionOf(project, 'termine'), { state: 'setup' }) assert.deepEqual(funktionOf({ projectId: 'p2', features: { news: { state: 'active' } } }, 'termine'), { state: 'off' }) assert.deepEqual(funktionOf({ projectId: 'p3' }, 'news'), { state: 'off' }) assert.deepEqual(funktionOf(null, 'news'), { state: 'off' }) }) test('aktionen: News unverändert – Aktivieren außer bei "active", Abschalten bei Bestellt, Einrichtung und Aktiv', () => { assert.deepEqual(aktionen('off', 'news'), ['activate']) assert.deepEqual(aktionen('ordered', 'news'), ['activate', 'disable']) assert.deepEqual(aktionen('setup', 'news'), ['activate', 'disable']) assert.deepEqual(aktionen('active', 'news'), ['disable']) assert.deepEqual(aktionen('disabled', 'news'), ['activate']) assert.deepEqual(aktionen(undefined, 'news'), ['activate']) }) test('aktionen: ohne Schlüssel wie News (auch mit undefiniertem Zustand)', () => { assert.deepEqual(aktionen('off'), ['activate']) assert.deepEqual(aktionen('ordered'), ['activate', 'disable']) assert.deepEqual(aktionen('setup'), ['activate', 'disable']) assert.deepEqual(aktionen('active'), ['disable']) assert.deepEqual(aktionen('disabled'), ['activate']) assert.deepEqual(aktionen(undefined), ['activate']) assert.deepEqual(aktionen('setup', undefined, { ticketMissing: true }), ['activate', 'disable'], 'ticketMissing betrifft nur den Terminplaner') }) test('aktionen: News ignoriert ticketMissing in jedem Zustand', () => { for (const state of ['off', 'ordered', 'setup', 'active', 'disabled', undefined]) { assert.deepEqual(aktionen(state, 'news', { ticketMissing: true }), aktionen(state, 'news'), String(state)) } }) test('aktionen: Terminplaner – bestellt nur Abschalten (kein Aktivieren ohne Zahlung), Einrichtung mit Abnahme', () => { assert.deepEqual(aktionen('off', 'termine'), ['activate']) assert.deepEqual(aktionen('ordered', 'termine'), ['disable'], 'Aktivieren würde eine spätere Zahlung im Portal verschlucken') assert.deepEqual(aktionen('setup', 'termine'), ['abnahme', 'disable']) assert.deepEqual(aktionen('active', 'termine'), ['disable']) assert.deepEqual(aktionen('disabled', 'termine'), ['activate']) assert.deepEqual(aktionen(undefined, 'termine'), ['activate']) }) test('aktionen: Terminplaner mit fehlendem Ticket bietet in der Einrichtung zuerst "ticket" an', () => { assert.deepEqual(aktionen('setup', 'termine', { ticketMissing: true }), ['ticket', 'abnahme', 'disable']) assert.deepEqual(aktionen('setup', 'termine', { ticketMissing: false }), ['abnahme', 'disable']) assert.deepEqual(aktionen('setup', 'termine', {}), ['abnahme', 'disable']) for (const state of ['off', 'ordered', 'active', 'disabled', undefined]) { assert.deepEqual(aktionen(state, 'termine', { ticketMissing: true }), aktionen(state, 'termine'), `${String(state)}: ticketMissing nur in setup`) } }) test('AKTION_LABEL: Beschriftung je Aktion', () => { assert.deepEqual(AKTION_LABEL, { activate: 'Aktivieren (ohne Zahlung)', disable: 'Abschalten', abnahme: 'Freischalten (Abnahme)', ticket: 'Ticket nachlegen', }) }) test('hindernisText: Text je Code, sonst leer', () => { assert.equal(hindernisText('kein_abo'), 'Kein laufendes Wartungs-Abo: Freischalten ist gesperrt.') assert.equal(hindernisText('keine_einstellung'), 'Noch keine Einstellungen gespeichert.') assert.equal(hindernisText('abo_unbekannt'), 'Abo gerade nicht prüfbar.') for (const code of ['x', '', null, undefined, 5, 'constructor', 'toString']) assert.equal(hindernisText(code), '', String(code)) }) // Antwort von GET /funktionen/:customerId/termine/anleitung (Portal, D8) in Kurzform const antwort = () => ({ stand: { version: 3, updatedAt: '2026-10-03T10:15:00.000Z' }, warnungen: ['Datenschutz-Entwurf', 'Ticket ist älter als der Stand'], abschnitte: [ { id: 'modell', titel: 'Modell', text: 'Modell-Text', kopierbar: false }, { id: 'snippet', titel: 'Einbau-Code (Snippet)', text: '', kopierbar: true }, { id: 'css', titel: 'Aussehen (CSS)', text: ':root {}', kopierbar: true }, { id: 'checkliste', titel: 'Abnahme-Checkliste', text: '- [ ] Punkt', kopierbar: false }, { id: 'datenschutz_sie', titel: 'Datenschutz (Sie)', text: 'Absatz Sie', kopierbar: true }, { id: 'datenschutz_du', titel: 'Datenschutz (Du)', text: 'Absatz Du', kopierbar: true }, ], text: 'GANZE ANLEITUNG', state: 'setup', ticketWoid: '100123', ticketVersion: 3, hindernis: null, }) test('anleitungKopien: zuerst "alles", dann die kopierbaren Abschnitte in Antwortreihenfolge mit festen Beschriftungen', () => { assert.deepEqual(anleitungKopien(antwort()), [ { id: 'alles', label: 'Alles kopieren', text: 'GANZE ANLEITUNG' }, { id: 'snippet', label: 'Einbau-Code kopieren', text: '' }, { id: 'css', label: 'CSS-Variablen kopieren', text: ':root {}' }, { id: 'datenschutz_sie', label: 'Datenschutz-Absatz (Sie) kopieren', text: 'Absatz Sie' }, { id: 'datenschutz_du', label: 'Datenschutz-Absatz (Du) kopieren', text: 'Absatz Du' }, ]) }) test('anleitungKopien: nicht kopierbare Abschnitte fehlen, unbekannte kopierbare heißen " kopieren"', () => { const a = antwort() a.abschnitte = [ { id: 'modell', titel: 'Modell', text: 'x', kopierbar: false }, { id: 'neu', titel: 'Neuer Teil', text: 'Inhalt', kopierbar: true }, { id: 'constructor', titel: 'Eigener Titel', text: 'y', kopierbar: true }, ] assert.deepEqual(anleitungKopien(a), [ { id: 'alles', label: 'Alles kopieren', text: 'GANZE ANLEITUNG' }, { id: 'neu', label: 'Neuer Teil kopieren', text: 'Inhalt' }, { id: 'constructor', label: 'Eigener Titel kopieren', text: 'y' }, ]) }) test('anleitungKopien: kopierbarer Abschnitt ohne Text entfällt, null und Antwort ohne Text ergeben []', () => { const a = antwort() a.abschnitte = [{ id: 'snippet', titel: 'Einbau-Code (Snippet)', text: '', kopierbar: true }] assert.deepEqual(anleitungKopien(a), [{ id: 'alles', label: 'Alles kopieren', text: 'GANZE ANLEITUNG' }]) assert.deepEqual(anleitungKopien(null), []) assert.deepEqual(anleitungKopien(undefined), []) assert.deepEqual(anleitungKopien({ ...antwort(), text: '' }), []) assert.deepEqual(anleitungKopien({ ...antwort(), text: undefined }), []) assert.deepEqual(anleitungKopien({ abschnitte: antwort().abschnitte }), []) }) test('anleitungPfad: Kunde und Projekt kodiert', () => { assert.equal(anleitungPfad('c1', 'p1'), '/funktionen/c1/termine/anleitung?projectId=p1') assert.equal(anleitungPfad('c/1', 'p&x=1 ü'), '/funktionen/c%2F1/termine/anleitung?projectId=p%26x%3D1%20%C3%BC') }) test('panelModell: Beispielantwort liefert alle sechs Felder', () => { const modell = panelModell(antwort(), { state: 'setup', hindernis: 'kein_abo' }) assert.deepEqual(Object.keys(modell).sort(), ['abschnitte', 'hinweis', 'kopf', 'kopien', 'ticketPfad', 'warnungen']) assert.equal(modell.kopf, 'Stand Version 3, 03.10.2026') assert.equal(modell.ticketPfad, '/tickets?woid=100123') assert.deepEqual(modell.warnungen, ['Datenschutz-Entwurf', 'Ticket ist älter als der Stand']) assert.equal(modell.hinweis, 'Kein laufendes Wartungs-Abo: Freischalten ist gesperrt.') assert.deepEqual(modell.abschnitte, [ { id: 'modell', titel: 'Modell', text: 'Modell-Text' }, { id: 'snippet', titel: 'Einbau-Code (Snippet)', text: '' }, { id: 'css', titel: 'Aussehen (CSS)', text: ':root {}' }, { id: 'checkliste', titel: 'Abnahme-Checkliste', text: '- [ ] Punkt' }, { id: 'datenschutz_sie', titel: 'Datenschutz (Sie)', text: 'Absatz Sie' }, { id: 'datenschutz_du', titel: 'Datenschutz (Du)', text: 'Absatz Du' }, ]) assert.deepEqual(modell.kopien, anleitungKopien(antwort())) }) test('panelModell: Kopf nach Berliner Kalendertag, ohne Datum nur die Version', () => { const kopf = (stand) => panelModell({ ...antwort(), stand }, {}).kopf assert.equal(kopf({ version: 12, updatedAt: '2026-10-03T23:30:00.000Z' }), 'Stand Version 12, 04.10.2026', 'nach Mitternacht in Berlin schon der nächste Tag') assert.equal(kopf({ version: 2, updatedAt: null }), 'Stand Version 2') assert.equal(kopf({ version: 2 }), 'Stand Version 2') assert.equal(kopf({ version: 2, updatedAt: 'kein Datum' }), 'Stand Version 2') assert.equal(panelModell({ ...antwort(), stand: undefined }, {}).kopf, '') assert.equal(panelModell({ ...antwort(), stand: {} }, {}).kopf, '', 'ohne Version kein Kopf') }) test('panelModell: null-Antwort ergibt ein leeres Modell', () => { const leer = { kopf: '', ticketPfad: null, warnungen: [], hinweis: '', kopien: [], abschnitte: [] } assert.deepEqual(panelModell(null), leer) assert.deepEqual(panelModell(undefined, undefined), leer) assert.deepEqual(panelModell(null, { hindernis: 'kein_abo' }), leer) }) test('panelModell: ohne ticketWoid kein Ticketpfad, ohne Hindernis kein Hinweis, Woid wird kodiert', () => { const ohne = panelModell({ ...antwort(), ticketWoid: null }, { state: 'setup' }) assert.equal(ohne.ticketPfad, null) assert.equal(ohne.hinweis, '') assert.equal(panelModell({ ...antwort(), ticketWoid: undefined }, null).ticketPfad, null) assert.equal(panelModell({ ...antwort(), ticketWoid: '' }, null).ticketPfad, null) assert.equal(panelModell({ ...antwort(), ticketWoid: 'a b/1' }, null).ticketPfad, '/tickets?woid=a%20b%2F1') assert.equal(panelModell(antwort(), { hindernis: 'unbekannt' }).hinweis, '') }) test('panelModell: fehlende Listen werden zu leeren Listen', () => { const modell = panelModell({ stand: { version: 1, updatedAt: null }, state: 'active' }, {}) assert.deepEqual(modell.warnungen, []) assert.deepEqual(modell.abschnitte, []) assert.deepEqual(modell.kopien, []) }) test('anleitungOffen: nur die Adresse mit ?anleitung= öffnet das Panel', () => { assert.equal(anleitungOffen('?tab=funktionen&anleitung=p%2F1', 'p/1'), true, 'kodiert') assert.equal(anleitungOffen('?tab=funktionen&anleitung=p/1', 'p/1'), true, 'unkodiert') assert.equal(anleitungOffen('?anleitung=p1', 'p1'), true) assert.equal(anleitungOffen('anleitung=p1', 'p1'), true, 'ohne Fragezeichen') assert.equal(anleitungOffen('?tab=funktionen&anleitung=p2', 'p1'), false, 'anderes Projekt') assert.equal(anleitungOffen('?anleitung=p10', 'p1'), false, 'kein Präfix-Treffer') assert.equal(anleitungOffen('?tab=funktionen', 'p1'), false) assert.equal(anleitungOffen('?anleitung=', 'p1'), false) assert.equal(anleitungOffen('', 'p1'), false) assert.equal(anleitungOffen(undefined, 'p1'), false) assert.equal(anleitungOffen(null, 'p1'), false) assert.equal(anleitungOffen('?anleitung=p1', ''), false) assert.equal(anleitungOffen('?anleitung=', ''), false, 'leeres Projekt öffnet nichts') assert.equal(anleitungOffen('?anleitung=p1', undefined), false) }) const projects = () => [ { projectId: 'p1', projectName: 'Eins', features: { news: { state: 'off' }, termine: { state: 'active', siteKey: 'abc' } } }, { projectId: 'p2', projectName: 'Zwei', features: { news: { state: 'active' }, termine: { state: 'off' } } }, ] test('mergeFunktion: übernimmt nur die gewählte Funktion, die andere bleibt', () => { const antwort = { projectId: 'p1', projectName: 'Eins (neu)', features: { news: { state: 'setup', ticketWoid: '7' }, termine: { state: 'off' } }, } const merged = mergeFunktion(projects(), 'p1', 'news', antwort) assert.deepEqual(merged[0].features.news, { state: 'setup', ticketWoid: '7' }) assert.deepEqual(merged[0].features.termine, { state: 'active', siteKey: 'abc' }) assert.equal(merged[0].projectName, 'Eins') assert.deepEqual(merged[1], projects()[1]) }) test('mergeFunktion: Terminplaner überschreibt News nicht', () => { const antwort = { projectId: 'p2', features: { news: { state: 'off' }, termine: { state: 'ordered' } } } const merged = mergeFunktion(projects(), 'p2', 'termine', antwort) assert.deepEqual(merged[1].features, { news: { state: 'active' }, termine: { state: 'ordered' } }) }) test('mergeFunktion: verändert die Eingabe nicht, ohne Antwort oder ohne Funktion bleibt alles', () => { const list = projects() const merged = mergeFunktion(list, 'p1', 'news', { projectId: 'p1', features: { news: { state: 'active' } } }) assert.notEqual(merged, list) assert.deepEqual(list, projects()) assert.deepEqual(mergeFunktion(list, 'p1', 'news', undefined), projects()) assert.deepEqual(mergeFunktion(list, 'p1', 'news', { projectId: 'p1', features: {} }), projects()) assert.deepEqual(mergeFunktion(list, 'unbekannt', 'news', { features: { news: { state: 'active' } } }), projects()) }) test('mergeFunktion: Projekt ohne bisherige features bekommt die Funktion', () => { const merged = mergeFunktion([{ projectId: 'p9' }], 'p9', 'termine', { features: { termine: { state: 'setup' } } }) assert.deepEqual(merged, [{ projectId: 'p9', features: { termine: { state: 'setup' } } }]) }) test('siteKeyOf: nur der Terminplaner zeigt seinen Site-Key, nur wenn das Portal einen nennt', () => { assert.equal(siteKeyOf({ state: 'active', siteKey: 'abcdefghijklmnop' }, 'termine'), 'abcdefghijklmnop') assert.equal(siteKeyOf({ state: 'active', siteKey: ' abcdefghijklmnop ' }, 'termine'), 'abcdefghijklmnop', 'getrimmt') assert.equal(siteKeyOf({ state: 'active', siteKey: 'abcdefghijklmnop' }, 'news'), '', 'News: kein Site-Key in der Tabelle') for (const feature of [{ state: 'off' }, { state: 'active', siteKey: null }, { state: 'active', siteKey: '' }, { state: 'active', siteKey: 5 }, null, undefined]) { assert.equal(siteKeyOf(feature, 'termine'), '', JSON.stringify(feature)) } }) test('abgeschaltetText: Grund und Löschdatum im Zustand "disabled"', () => { const text = (disabledReason, loeschungAm) => abgeschaltetText({ state: 'disabled', disabledReason, loeschungAm }) assert.equal(text('abo_ende', '2026-11-04'), 'Abgeschaltet (Abo beendet), Löschung am 04.11.2026') assert.equal(text('abo_ende', undefined), 'Abgeschaltet (Abo beendet)') assert.equal(text('abo_ende_vor_einrichtung', '2026-11-04'), 'Abgeschaltet (Abo endete vor Einrichtung, Erstattung prüfen), Löschung am 04.11.2026') assert.equal(text('abo_ende_vor_einrichtung', null), 'Abgeschaltet (Abo endete vor Einrichtung, Erstattung prüfen)') assert.equal(text('team', '2026-12-01'), 'Abgeschaltet (Team), Löschung am 01.12.2026') assert.equal(text('team', null), 'Abgeschaltet (Team)') assert.equal(text('kunde_geloescht', '2026-12-31'), 'Abgeschaltet (Kunde gelöscht), Löschung am 31.12.2026') assert.equal(text('kunde_geloescht', ''), 'Abgeschaltet (Kunde gelöscht)') }) test('abgeschaltetText: ohne Grund nur "Abgeschaltet", mit Datum "Abgeschaltet, Löschung am …"', () => { assert.equal(abgeschaltetText({ state: 'disabled' }), 'Abgeschaltet') assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: null, loeschungAm: null }), 'Abgeschaltet') assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: '', loeschungAm: '' }), 'Abgeschaltet') assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: null, loeschungAm: '2026-11-04' }), 'Abgeschaltet, Löschung am 04.11.2026') assert.equal(abgeschaltetText({ state: 'disabled', loeschungAm: '2026-01-09' }), 'Abgeschaltet, Löschung am 09.01.2026') }) test('abgeschaltetText: unbekannter Grund wird wie "ohne Grund" behandelt', () => { assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 'frist' }), 'Abgeschaltet') assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 'frist', loeschungAm: '2026-11-04' }), 'Abgeschaltet, Löschung am 04.11.2026') assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 'constructor' }), 'Abgeschaltet', 'kein Treffer über die Prototypenkette') assert.equal(abgeschaltetText({ state: 'disabled', disabledReason: 5 }), 'Abgeschaltet') }) test('abgeschaltetText: außerhalb von "disabled" leer, auch mit Grund und Datum', () => { for (const state of ['active', 'setup', 'ordered', 'off', undefined, null]) { assert.equal(abgeschaltetText({ state, disabledReason: 'abo_ende', loeschungAm: '2026-11-04' }), '', String(state)) } for (const feature of [{}, null, undefined]) assert.equal(abgeschaltetText(feature), '', JSON.stringify(feature)) }) // --- Staff-Aktivierung des Terminplaners nur mit Zustimmung in Textform (F13b; Portal F13a: GET ... => termine) --- // Feld "termine" der Portal-Antwort GET /funktionen/:customerId const termine = (extra = {}) => ({ textformNoetig: true, avvVersion: '2026-10-05', links: { lb: 'https://project.webklar.com/rechtliches/leistungsbeschreibung-terminplaner-2026-10-05.html', avv: 'https://project.webklar.com/rechtliches/avv-terminplaner-2026-10-05.html', }, ...extra, }) test('textformNoetig: nur "Aktivieren (ohne Zahlung)" des Terminplaners und nur, wenn das Portal es verlangt', () => { assert.equal(textformNoetig('termine', 'activate', termine()), true) assert.equal(textformNoetig('termine', 'activate', termine({ textformNoetig: false })), false) assert.equal(textformNoetig('termine', 'activate', undefined), false, 'altes Portal ohne Feld: wie bisher') assert.equal(textformNoetig('termine', 'activate', null), false) assert.equal(textformNoetig('termine', 'activate', termine({ textformNoetig: 'true' })), false, 'nur echtes true') for (const action of ['disable', 'abnahme', 'ticket']) assert.equal(textformNoetig('termine', action, termine()), false, action) for (const action of ['activate', 'disable']) assert.equal(textformNoetig('news', action, termine()), false, `News ${action}`) assert.equal(textformNoetig(undefined, 'activate', termine()), false) }) test('termineChatTemplate: wörtlich, mit Fassung und den beiden versionierten Adressen', () => { const { lb, avv } = termine().links assert.equal( termineChatTemplate('2026-10-05', { lb, avv }), 'Damit wir den Terminplaner für Dich einschalten können, brauchen wir Deine Zustimmung in Textform. ' + 'Es gelten die Leistungsbeschreibung Terminplaner und der Auftragsverarbeitungsvertrag Terminplaner in der Fassung 2026-10-05: ' + `${lb} · ${avv}. ` + 'Bitte antworte hier mit „Ich stimme zu“ und bestätige, dass Dein Angebot nicht im Gesundheitsbereich liegt.', ) const text = termineChatTemplate('2026-10-05', { lb, avv }) assert.match(text, /leistungsbeschreibung-terminplaner-2026-10-05\.html/) assert.match(text, /avv-terminplaner-2026-10-05\.html/) assert.ok(!text.includes('undefined') && !text.includes('{')) }) test('termineTextformLabel: Dialogtext des Kästchens nennt beide Unterlagen, die Fassung und den Ausschluss des Gesundheitsbereichs', () => { const l = termineTextformLabel('2026-10-05') assert.equal(l.lb, 'Leistungsbeschreibung Terminplaner') assert.equal(l.avv, 'Auftragsverarbeitungsvertrag Terminplaner') assert.equal( l.before + l.lb + l.between + l.avv + l.after, 'Der Kunde hat der Leistungsbeschreibung Terminplaner und dem Auftragsverarbeitungsvertrag Terminplaner (Fassung 2026-10-05) ' + 'in Textform zugestimmt und bestätigt, dass sein Angebot nicht im Gesundheitsbereich liegt.', ) }) test('textformAnsicht: Fassung, geprüfte https-Adressen, Label und Vorlage aus dem Portal-Feld', () => { const a = textformAnsicht(termine()) assert.equal(a.version, '2026-10-05') assert.deepEqual(a.links, termine().links) assert.deepEqual(a.label, termineTextformLabel('2026-10-05')) assert.equal(a.vorlage, termineChatTemplate('2026-10-05', termine().links)) }) test('textformAnsicht: Adressen ohne https werden nie Link, ohne beide Adressen keine Vorlage', () => { const schlecht = textformAnsicht(termine({ links: { lb: 'javascript:alert(1)', avv: 'http://project.webklar.com/x.html' } })) assert.deepEqual(schlecht.links, { lb: '', avv: '' }) assert.equal(schlecht.vorlage, '', 'eine Vorlage mit leeren Adressen geht nie an den Kunden') const halb = textformAnsicht(termine({ links: { lb: termine().links.lb } })) assert.equal(halb.links.avv, '') assert.equal(halb.vorlage, '') for (const leer of [undefined, null, {}, { links: null }]) { const a = textformAnsicht(leer) assert.equal(a.version, '') assert.deepEqual(a.links, { lb: '', avv: '' }) assert.equal(a.vorlage, '') } }) test('textformBereit: canSubmitTextform mit minDate = Fassung (Grenzen eingeschlossen), today = heute in Berlin', () => { const t = termine() const ok = { confirmed: true, date: '2026-10-06', channel: 'chat', today: '2026-10-07' } assert.equal(textformBereit(ok, t), true) assert.equal(textformBereit({ ...ok, date: '2026-10-05' }, t), true, 'Tag der Fassung selbst') assert.equal(textformBereit({ ...ok, date: '2026-10-07' }, t), true, 'heute') assert.equal(textformBereit({ ...ok, channel: 'email' }, t), true) assert.equal(textformBereit({ ...ok, date: '2026-10-04' }, t), false, 'vor der Fassung') assert.equal(textformBereit({ ...ok, date: '2026-10-08' }, t), false, 'Zukunft') assert.equal(textformBereit({ ...ok, confirmed: false }, t), false) assert.equal(textformBereit({ ...ok, date: '' }, t), false) assert.equal(textformBereit({ ...ok, date: 'morgen' }, t), false) assert.equal(textformBereit({ ...ok, channel: '' }, t), false) assert.equal(textformBereit({ ...ok, channel: 'telefon' }, t), false) }) test('textformBereit: ohne Fassung im Portal-Feld nie bereit (fail closed)', () => { const ok = { confirmed: true, date: '2026-10-06', channel: 'chat', today: '2026-10-07' } assert.equal(textformBereit(ok, termine({ avvVersion: '' })), false) assert.equal(textformBereit(ok, undefined), false) assert.equal(textformBereit(ok, null), false) }) test('featureBody: Aktivieren mit Textform trägt Datum und Weg, sonst bleibt der Rumpf wie bisher', () => { assert.deepEqual( featureBody({ projectId: 'p1', action: 'activate', note: 'Pilot', textformDate: '2026-10-06', textformChannel: 'chat' }), { projectId: 'p1', action: 'activate', note: 'Pilot', textformDate: '2026-10-06', textformChannel: 'chat' }, ) assert.deepEqual(featureBody({ projectId: 'p1', action: 'activate', note: '' }), { projectId: 'p1', action: 'activate', note: '' }) assert.deepEqual(featureBody({ projectId: 'p1', action: 'disable', note: 'x' }), { projectId: 'p1', action: 'disable', note: 'x' }) assert.deepEqual( featureBody({ projectId: 'p1', action: 'disable', note: 'x', textformDate: '2026-10-06', textformChannel: 'chat' }), { projectId: 'p1', action: 'disable', note: 'x' }, 'Abschalten sendet nie Textform-Angaben', ) assert.deepEqual( featureBody({ projectId: 'p1', action: 'activate', note: '', textformDate: '2026-10-06', textformChannel: '' }), { projectId: 'p1', action: 'activate', note: '' }, 'halbe Angaben gehen nicht mit', ) })