Files
tickte-system/tests/unit/funktionenView.test.mjs
T
ClaudeandClaude Opus 5.5 994347408d Öffnungszeiten: Register und Texte im Ticketsystem
Funktionen-Reiter kennt "Öffnungszeiten live" (zeiten) hinter dem Terminplaner:
Bestätigungstexte, Site-Key, Aktionen je Zustand (Abnahme nur ohne Hindernis
"zeiten_fehlen"). Neues Modul zeitenView.js: Statustext (Einbau läuft / wartet
auf die Zeiten des Kunden / Abo-Ende mit Frist), navSelektorFehler (Spiegel
der Portal-Regel) und teamBody für PATCH .../zeiten/team. Protokoll-Label
für zeiten_gespeichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:02:30 +00:00

527 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, Terminplaner und Öffnungszeiten live (in dieser Reihenfolge) 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?',
},
},
{
key: 'zeiten',
label: 'Öffnungszeiten live',
bestaetigen: {
activate: 'Öffnungszeiten live ohne Zahlung aktivieren? Es startet der Einbau: Das Einbau-Ticket wird angelegt und die Seite, wenn möglich, neu gebaut. Freigeschaltet wird, sobald das Ticket geschlossen ist und der Kunde seine Zeiten eingetragen hat.',
disable: 'Öffnungszeiten live abschalten? Leisten und Live-Anzeige verschwinden von der Website, die feste Ersatz-Tabelle bleibt stehen. Die Angaben des Kunden bleiben gespeichert. Weicht der Wochenplan von der Ersatz-Tabelle ab, wird ein Ticket „Ersatz-Tabelle aktualisieren“ angelegt.',
abnahme: 'Öffnungszeiten live freischalten? Der Kunde bekommt die Mail „bereit“ und der Einbau gilt als abgenommen.',
ticket: 'Fehlendes Einbau-Ticket neu anlegen?',
},
},
])
})
test('FUNKTIONEN: Schlüssel sind eindeutig und jede Funktion hat Aktivieren und Abschalten bestätigt', () => {
assert.deepEqual(FUNKTIONEN.map((f) => f.key), ['news', 'termine', 'zeiten'])
for (const f of FUNKTIONEN) {
assert.ok(f.bestaetigen.activate && f.bestaetigen.disable, f.key)
}
const zeiten = FUNKTIONEN.find((f) => f.key === 'zeiten').bestaetigen
for (const text of Object.values(zeiten)) assert.doesNotMatch(text, /24 Uhr/, 'nie "24 Uhr"')
})
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('aktionen: Öffnungszeiten live – aus nur Aktivieren, bestellt nur Abschalten, Einrichtung mit Abnahme, aktiv nur Abschalten, abgeschaltet nur Aktivieren', () => {
assert.deepEqual(aktionen('off', 'zeiten'), ['activate'])
assert.deepEqual(aktionen('ordered', 'zeiten'), ['disable'], 'wie beim Terminplaner: Aktivieren würde eine spätere Zahlung verschlucken')
assert.deepEqual(aktionen('setup', 'zeiten'), ['abnahme', 'disable'])
assert.deepEqual(aktionen('active', 'zeiten'), ['disable'])
assert.deepEqual(aktionen('disabled', 'zeiten'), ['activate'])
assert.deepEqual(aktionen(undefined, 'zeiten'), ['activate'])
})
test('aktionen: Öffnungszeiten live bietet die Abnahme nur ohne Hindernis an (wartet auf die Zeiten des Kunden: nur Abschalten)', () => {
assert.deepEqual(aktionen('setup', 'zeiten', { hindernis: 'zeiten_fehlen' }), ['disable'])
assert.deepEqual(aktionen('setup', 'zeiten', { hindernis: null }), ['abnahme', 'disable'])
assert.deepEqual(aktionen('setup', 'zeiten', { hindernis: undefined }), ['abnahme', 'disable'])
assert.deepEqual(aktionen('setup', 'zeiten', { ticketMissing: true, hindernis: 'zeiten_fehlen' }), ['ticket', 'disable'])
for (const state of ['off', 'ordered', 'active', 'disabled', undefined]) {
assert.deepEqual(aktionen(state, 'zeiten', { hindernis: 'zeiten_fehlen' }), aktionen(state, 'zeiten'), `${String(state)}: hindernis nur in setup`)
}
})
test('aktionen: Öffnungszeiten live mit fehlendem Ticket bietet in der Einrichtung zuerst "ticket" an (wie der Terminplaner)', () => {
assert.deepEqual(aktionen('setup', 'zeiten', { ticketMissing: true }), ['ticket', 'abnahme', 'disable'])
assert.deepEqual(aktionen('setup', 'zeiten', { ticketMissing: false }), ['abnahme', 'disable'])
for (const state of ['off', 'ordered', 'active', 'disabled', undefined]) {
assert.deepEqual(aktionen(state, 'zeiten', { ticketMissing: true }), aktionen(state, 'zeiten'), `${String(state)}: ticketMissing nur in setup`)
}
})
test('aktionen: Terminplaner und News ignorieren hindernis (der Terminplaner zeigt die Abnahme weiter, das Portal sperrt)', () => {
for (const hindernis of ['kein_abo', 'keine_einstellung', 'abo_unbekannt', 'zeiten_fehlen', null]) {
assert.deepEqual(aktionen('setup', 'termine', { hindernis }), ['abnahme', 'disable'], String(hindernis))
assert.deepEqual(aktionen('setup', 'news', { hindernis }), ['activate', 'disable'], String(hindernis))
}
})
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: '<script></script>', 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: '<script></script>' },
{ 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 "<Titel> 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: '<script></script>' },
{ 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=<projectId> ö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('siteKeyOf liefert für zeiten den Schlüssel, für news weiter ""', () => {
assert.equal(siteKeyOf({ state: 'setup', siteKey: 'abcdefghijklmnop' }, 'zeiten'), 'abcdefghijklmnop')
assert.equal(siteKeyOf({ state: 'setup', siteKey: ' abcdefghijklmnop ' }, 'zeiten'), 'abcdefghijklmnop', 'getrimmt')
assert.equal(siteKeyOf({ state: 'active', siteKey: 'abcdefghijklmnop' }, 'news'), '')
assert.equal(siteKeyOf({ state: 'active', siteKey: 'abcdefghijklmnop' }, undefined), '')
assert.equal(siteKeyOf({ state: 'active', siteKey: 'abcdefghijklmnop' }, 'unbekannt'), '')
for (const feature of [{ state: 'off' }, { state: 'setup', siteKey: null }, { state: 'setup', siteKey: '' }, { state: 'setup', siteKey: 5 }, null, undefined]) {
assert.equal(siteKeyOf(feature, 'zeiten'), '', 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',
)
})