Öffnungszeiten: Tests und Belehrung nach dem Merge mit TP2

Zeiten nutzen die echte Widerrufsbelehrung wie der Terminplaner (der Platzhalter BELEHRUNG_PLATZHALTER ist
mit TP2 F7 entfallen). Gepinnte Listen als Vereinigung beider Seiten: K15-Blöcke, T16-Blöcke, legal-Schlüssel der
Projektliste, hindernis in der Team-Ansicht für alle Funktionen. Neue Tests: Abnahme (Freischalten) der Zeiten über den
Haken freischaltbar und mit Ersatz-Hash, abnahme gibt für Zeiten nie ab.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 04:43:20 +00:00
1 parent 1d5a565e9f
commit 019833d385
6 files changed
+71 -19

No files matched your search

+6 -10
View File
@@ -68,12 +68,6 @@ async function zeitenNachFreischaltung(doc) {
await setzeErsatzHash(doc, wochenplanHash((await ladeZeiten(doc.$id))?.daten ?? null))
}
/**
* Platzhalter der Widerrufsbelehrung der Oeffnungszeiten (Haken belehrung von zeiten). Der Terminplaner liefert seine echte
* Belehrung mit termineBelehrung (Task F7); fuer die Oeffnungszeiten bleibt dieser Platzhalter, bis ihr Belehrungstext feststeht.
*/
export const BELEHRUNG_PLATZHALTER = 'ENTWURF (Block F): Widerrufsbelehrung mit Muster-Widerrufsformular.'
/**
* Vertragsunterlagen des Terminplaners abrufbar? (Stufe 5b des Checkouts, vor jeder Stripe-Sitzung): HTML-Seiten und PDFs der
* aktuellen Fassung sind da und lesbar (legalDocs.featureLegalReady, Task F8; fail-closed, wirft nie). Geladen wird legalDocs erst
@@ -87,7 +81,8 @@ async function termineUnterlagenBereit() {
/**
* Widerrufsbelehrung mit Muster-Widerrufsformular der Funktionen (Gesetzesmuster, Sie-Form), die die Bestellseite vor dem Knopf
* zeigt und der Zustimmungsnachweis speichert: dieselbe Zeichenkette an beiden Stellen (Task F7, Text aus Task F3). Geladen
* wird sie erst beim Aufruf (kein Importzyklus).
* wird sie erst beim Aufruf (kein Importzyklus). Sie gilt fuer jede kostenpflichtige Funktion; auch die Oeffnungszeiten live
* (Haken belehrung von zeiten) liefern sie.
*/
async function termineBelehrung() {
const { widerrufsbelehrungFunktion } = await import('./termine/rechtstexte.js')
@@ -109,12 +104,13 @@ const STANDARD_HAKEN = {
belehrung: termineBelehrung,
freischaltbar: termineFreischaltbar,
},
// Oeffnungszeiten live: keine Bestellpruefung (die Einstellung entsteht erst im Editor); belehrung und
// unterlagenBereit wie beim Terminplaner; freischaltbar: erst mit eingetragenen Zeiten; nachFreischaltung: Ersatz-Hash merken
// Oeffnungszeiten live: keine Bestellpruefung (die Einstellung entsteht erst im Editor); belehrung wie beim Terminplaner,
// unterlagenBereit immer (die Oeffnungszeiten haben nur die Leistungsbeschreibung als Seite, keine PDFs); freischaltbar: erst mit
// eingetragenen Zeiten; nachFreischaltung: Ersatz-Hash merken
zeiten: {
verfuegbar: (email) => zeitenFuerKunde(email),
unterlagenBereit: async () => true,
belehrung: async () => BELEHRUNG_PLATZHALTER,
belehrung: termineBelehrung,
freischaltbar: zeitenFreischaltbar,
nachFreischaltung: zeitenNachFreischaltung,
},
+3 -1
View File
@@ -115,8 +115,10 @@ test('liste_legal: legal.termine mit Fassung, https-Links mit Fassung und der Be
const { legalPaths } = await import('../server/services/legalDocs.js')
const V = config.termine.avvVersion
const body = await get('/api/funktionen', { cookie })
assert.deepEqual(Object.keys(body.legal).sort(), ['news', 'termine'])
assert.deepEqual(Object.keys(body.legal).sort(), ['news', 'termine', 'zeiten'])
assert.equal(body.legal.news, null)
// die Oeffnungszeiten haben keine Rechtsdokumente zum Anhaengen (avv und widerruf null): kein Eintrag wie beim Terminplaner
assert.equal(body.legal.zeiten, null)
const termine = body.legal.termine
assert.deepEqual(Object.keys(termine).sort(), ['belehrung', 'links', 'version'])
assert.equal(termine.version, V)
+32
View File
@@ -272,6 +272,38 @@ test('nachFreischaltung des Registers wird je freigeschaltetem Dokument mit dem
assert.match(logText(), /Haken kaputt/)
})
// ------------------------------------------------------------------------------ Freischalten (Abnahme) durch das Team
// Merge mit TP2: freischaltenNachAbnahme (WOMS "Freischalten (Abnahme)") gilt fuer jede Funktion mit ticketFreischaltung. Die
// Voraussetzung der Zeiten ist ihr Haken freischaltbar (eingetragene Zeiten), nicht Abo und Einstellung des Terminplaners.
test('Abnahme der Zeiten: ohne eingetragene Zeiten 409 und alles bleibt setup, auch ohne laufendes Wartungs-Abo', async () => {
const id = await einrichtung({ ticket: 'Open' })
const projectId = feld(id, 'projectId')
await assert.rejects(sf.freischaltenNachAbnahme({ customerId: 'c1', projectId, key: 'zeiten', by: 'Kenso', now: JETZT }), (err) => {
assert.equal(err.status, 409)
assert.match(err.message, /Voraussetzungen/)
return true
})
assert.equal(zustand(id), 'setup')
assert.equal(sink.sent.length, 0)
})
test('Abnahme der Zeiten: mit eingetragenen Zeiten active, K16, Ersatz-Hash (nachFreischaltung) und ohne Abo-Prüfung des Terminplaners', async () => {
const id = await einrichtung({ ticket: 'Open', zeiten: WOCHE })
const projectId = feld(id, 'projectId')
const vorher = await ladeZeiten(id)
assert.equal(vorher.ersatzHash, '')
const { doc, activated } = await sf.freischaltenNachAbnahme({ customerId: 'c1', projectId, key: 'zeiten', by: 'Kenso', now: JETZT })
assert.equal(activated, true)
assert.equal(doc.state, 'active')
assert.equal(zustand(id), 'active')
assert.equal(k16().length, 1)
assert.equal((await ladeZeiten(id)).ersatzHash, wochenplanHash(vorher.daten))
// schon aktiv: Erfolg ohne zweite Mail
assert.equal((await sf.freischaltenNachAbnahme({ customerId: 'c1', projectId, key: 'zeiten', now: JETZT })).activated, false)
assert.equal(k16().length, 1)
})
// ------------------------------------------------------------------------------------------- Selbstheilung
test('Selbstheilung: setup ohne ticketId ⇒ der Job legt das Ticket an (und sendet die K24)', async () => {
+6 -5
View File
@@ -330,10 +330,11 @@ test('K15: Block behalten ersetzt den Einrichtungstext, ohne ihn bleibt die Mail
const feature = reg.featureConfig('zeiten')
const ctx = { kundeName: 'Silke Müller', projekt: 'Praxis Muster', invoiceUrl: 'https://invoice.stripe.test/i/1' }
const neu = sf.bestellMail(feature, ctx)
assert.deepEqual(neu.blocks, { rechnung: true, behalten: false, neu: true })
// anlagen, anhang und zustimmungen (TP2 F8) bleiben aus: die Oeffnungszeiten haben keine Rechtsdokumente zum Anhaengen
assert.deepEqual(neu.blocks, { rechnung: true, anlagen: false, anhang: false, zustimmungen: false, behalten: false, neu: true })
assert.deepEqual(sf.bestellMail(feature, { ...ctx, behalten: false }).blocks, neu.blocks)
const behalten = sf.bestellMail(feature, { ...ctx, behalten: true })
assert.deepEqual(behalten.blocks, { rechnung: true, behalten: true, neu: false })
assert.deepEqual(behalten.blocks, { rechnung: true, anlagen: false, anhang: false, zustimmungen: false, behalten: true, neu: false })
assert.deepEqual(behalten.values, neu.values, 'dieselben Werte, nur der Wortlaut wechselt')
const textNeu = render(neu)
@@ -359,7 +360,7 @@ test('K15: Block behalten ersetzt den Einrichtungstext, ohne ihn bleibt die Mail
test('K15: die Blöcke behalten und neu stehen im Kopf der Vorlage', () => {
const tpl = loadTemplate('K15')
assert.deepEqual(tpl.blocks, ['rechnung', 'behalten', 'neu'])
assert.deepEqual(tpl.blocks, ['rechnung', 'behalten', 'neu', 'anlagen', 'anhang', 'zustimmungen'])
// jeder Block erscheint, und in der Behalten-Fassung (neu aus) fehlt jeder Rest von zeit/danach
const behalten = renderMail('K15', Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`])), { behalten: true })
assert.deepEqual(behalten.problems, [])
@@ -368,7 +369,7 @@ test('K15: die Blöcke behalten und neu stehen im Kopf der Vorlage', () => {
test('T16: Blöcke abo und team nennen den Weg ohne Zahlung', () => {
const tpl = loadTemplate('T16')
assert.deepEqual(tpl.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit', 'abo', 'team'])
assert.deepEqual(tpl.blocks, ['zahlung_fehlgeschlagen', 'ungeklaert', 'kein_abo', 'bereits_aktiv', 'anleitung', 'gesundheit', 'abo', 'team', 'anhang_fehlt'])
const werte = { kunde_name: 'Silke Müller', email: 'silke@example.com', projekt: 'Praxis Muster', funktion: 'Öffnungszeiten live', woid: '10042', sitekey: 'abc', sitzung: '', zustand: '', anleitung_url: '', gesundheit_text: '' }
const aus = { zahlung_fehlgeschlagen: false, ungeklaert: false, kein_abo: false, bereits_aktiv: false, anleitung: false, gesundheit: false }
const abo = renderMail('T16', werte, { ...aus, abo: true, team: false })
@@ -382,5 +383,5 @@ test('T16: Blöcke abo und team nennen den Weg ohne Zahlung', () => {
const keiner = renderMail('T16', werte, aus)
assert.doesNotMatch(keiner.text, /im Wartungs-Abo enthalten|vom Team freigeschaltet/i)
// sendT16 führt beide Blöcke als Vorgabe (aus), damit jede Team-Mail sie nennt
assert.match(readFileSync(new URL('../server/services/siteFeatures.js', import.meta.url), 'utf8'), /gesundheit: false, abo: false, team: false, \.\.\.bloecke/)
assert.match(readFileSync(new URL('../server/services/siteFeatures.js', import.meta.url), 'utf8'), /abo: false,\s+team: false,\s+\.\.\.bloecke/)
})
-1
View File
@@ -110,7 +110,6 @@ test('Standard-Haken: verfuegbar, unterlagenBereit, belehrung, freischaltbar, na
const termine = reg.featureHaken('termine')
assert.equal(await haken.unterlagenBereit(), true)
assert.equal(await haken.belehrung(), await termine.belehrung(), 'wie beim Terminplaner')
assert.equal(await haken.belehrung(), reg.BELEHRUNG_PLATZHALTER)
// kein Platzhalter mehr (der alte Haken lieferte immer true): Verhalten und Nacharbeit prüft test/zeitenFreischalten.test.js
for (const name of ['freischaltbar', 'nachFreischaltung']) assert.equal(typeof haken[name], 'function', name)
})
+24 -2
View File
@@ -180,6 +180,27 @@ test('Team aktiviert eine abgeschaltete Funktion wieder: setup, Abschaltgrund ge
assert.match(doc.adminNote, / Andrej: activate \| alte Notiz$/)
})
// Merge mit TP2: die Aktionen abnahme und ticket gibt es fuer jede Funktion mit ticketFreischaltung; fuer die Zeiten darf
// abnahme weder abschalten noch den Gratis-Pfad nehmen, sondern gibt in setup frei (Haken freischaltbar, Ersatz-Hash)
test('Team „Freischalten (Abnahme)“ der Zeiten: ohne Zeiten 409 (bleibt setup), mit Zeiten active mit Ersatz-Hash, nie disabled', async () => {
const doc = featureDoc('p1', { ticketId: 't_offen', einbau: 'angestossen' })
aw.seed('workorders', { $id: 't_offen', woid: '10002', topic: 'Zeiten', status: 'Open', customerId: 'c1' })
aw.seed('portalFeatures', doc)
const zu = await aktion('abnahme')
assert.equal(zu.status, 409, JSON.stringify(zu.body))
assert.equal(zeitenDoc().state, 'setup')
assert.equal(sink.sent.length, 0)
await mitZeiten(doc, EIN_PLAN())
const ok = await aktion('abnahme')
assert.equal(ok.status, 200, JSON.stringify(ok.body))
assert.equal(ok.body.projects[0].features.zeiten.state, 'active')
assert.equal(zeitenDoc().state, 'active')
assert.match((await ladeZeiten(doc.$id)).ersatzHash, /^[0-9a-f]{16}$/)
assert.equal(sink.sent.length, 1, 'K16 an den Kunden')
assert.equal(ok.body.ersatzTicket, undefined, 'abnahme legt kein Ersatz-Ticket an')
})
test('Aktivieren: fremdes oder unbekanntes Projekt 403, unbekannte Aktion 400, ohne Anmeldung 401, nichts entsteht', async () => {
assert.equal((await aktion('activate', {}, { projectId: 'p3' })).status, 403)
assert.equal((await aktion('activate', {}, { projectId: 'gibtsnicht' })).status, 403)
@@ -527,9 +548,10 @@ test('adminView zeigt Pläne, team und hindernis (nur für zeiten)', async () =>
assert.equal(z2.teamVersion, 0)
assert.equal(z2.hindernis, 'zeiten_fehlen')
// andere Funktionen bekommen die Felder nicht
// andere Funktionen bekommen die Zeiten-Felder nicht; hindernis tragen seit TP2 alle (null ausser beim Terminplaner in setup)
for (const key of ['news', 'termine']) {
for (const feld of ['plaene', 'team', 'teamVersion', 'hindernis']) assert.equal(feld in projekte[0].features[key], false, `${key}.${feld}`)
for (const feld of ['plaene', 'team', 'teamVersion']) assert.equal(feld in projekte[0].features[key], false, `${key}.${feld}`)
assert.equal(projekte[0].features[key].hindernis, null, `${key}.hindernis`)
}
})