From 019833d385549a2c4c00638d4d66454829af9366 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 04:43:20 +0000 Subject: [PATCH] =?UTF-8?q?=C3=96ffnungszeiten:=20Tests=20und=20Belehrung?= =?UTF-8?q?=20nach=20dem=20Merge=20mit=20TP2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/services/featureRegistry.js | 16 ++++++--------- test/funktionenRoutes.test.js | 4 +++- test/zeitenFreischalten.test.js | 32 ++++++++++++++++++++++++++++++ test/zeitenMails.test.js | 11 +++++----- test/zeitenRegister.test.js | 1 - test/zeitenTeam.test.js | 26 ++++++++++++++++++++++-- 6 files changed, 71 insertions(+), 19 deletions(-) diff --git a/server/services/featureRegistry.js b/server/services/featureRegistry.js index ac4b565..9a4f856 100644 --- a/server/services/featureRegistry.js +++ b/server/services/featureRegistry.js @@ -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, }, diff --git a/test/funktionenRoutes.test.js b/test/funktionenRoutes.test.js index 18245a2..493225e 100644 --- a/test/funktionenRoutes.test.js +++ b/test/funktionenRoutes.test.js @@ -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) diff --git a/test/zeitenFreischalten.test.js b/test/zeitenFreischalten.test.js index fd6d47b..24c9b33 100644 --- a/test/zeitenFreischalten.test.js +++ b/test/zeitenFreischalten.test.js @@ -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 () => { diff --git a/test/zeitenMails.test.js b/test/zeitenMails.test.js index 3dae0e5..56fbc97 100644 --- a/test/zeitenMails.test.js +++ b/test/zeitenMails.test.js @@ -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/) }) diff --git a/test/zeitenRegister.test.js b/test/zeitenRegister.test.js index 24a3813..22373d5 100644 --- a/test/zeitenRegister.test.js +++ b/test/zeitenRegister.test.js @@ -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) }) diff --git a/test/zeitenTeam.test.js b/test/zeitenTeam.test.js index 04dcbac..4d5072b 100644 --- a/test/zeitenTeam.test.js +++ b/test/zeitenTeam.test.js @@ -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`) } })