From 65b1389b5555111ec3c6db34c0d4ddcc34364510 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 15:47:05 +0000 Subject: [PATCH] Abo-Fassung 6: Fassungstabelle, aktuelleAboFassung, termsVersionNeu Co-Authored-By: Claude Sonnet 5.5 --- server/config.js | 3 ++ server/services/products.js | 88 +++++++++++++++++++++++++++---------- test/products.test.js | 45 +++++++++++++++++++ 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/server/config.js b/server/config.js index c2f2549..8c16721 100644 --- a/server/config.js +++ b/server/config.js @@ -854,6 +854,9 @@ export const config = { abo: { // Bei jeder Textaenderung an Bedingungen oder Bestellseite hochziehen (wird mit der Zustimmung gespeichert) termsVersion: '2026-09-30-v5', + // Fassung 6 der Abo-Bedingungen: gilt nur fuer Sitzungen mit neuer Ansicht (aktuelleAboFassung in products.js). + // Entwurfstag; bei der Freigabe durch den Inhaber das Datum auf den Freigabetag setzen (P8 Task 11, Ablauf) + termsVersionNeu: '2026-10-16-v6', // Mindestlaufzeit ab Leistungsbeginn; Kuendigung zum Ende bis 1 Monat vorher, danach monatlich minTermMonths: products.wartung.minTermMonths, minutesPerMonth: products.wartung.minutesPerMonth, diff --git a/server/services/products.js b/server/services/products.js index 64d9e2e..5831a7e 100644 --- a/server/services/products.js +++ b/server/services/products.js @@ -1,6 +1,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { config } from '../config.js' +import { neueAnsicht } from './kundenansicht/schalter.js' /** * Abo-Produkte (Hosting-Abo, Wartungs-Abo) und E-Mail-Zusatzspeicher. @@ -28,43 +29,86 @@ export function getProduct(key) { /** Die archivierte Fassung 3 (public/vertragsunterlagen/v3): gilt fuer jede Zustimmung ohne Fassung "-v4"/"-v5". */ export const TERMS_VERSION_V3 = '2026-09-25-v3' +/** + * Fassungstabelle der Abo-Bedingungen (V12). Reihenfolge = Suche: der erste Eintrag, dessen muster auf die + * gespeicherte Fassung passt (nur als Endung, nie "-v60" oder "v6-..."), sonst der Eintrag sonst (Fassung 3). + * Fassung 6 liegt bis zum Umschalten (P10) im Vorschaupfad /neu/rechtliches/; Fassung 5 bleibt an den heutigen + * Adressen, archiv nennt die Adressen der Kopie unter public/vertragsunterlagen/v5/ (erst P10 schaltet sie ein). + * endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) fuer die Mails K02/K03/T05/T07 (F08). + */ +export const FASSUNGEN = Object.freeze([ + Object.freeze({ + muster: /-v6$/, + nummer: 6, + url: '/neu/rechtliches/abo-bedingungen.html', + pdfFile: 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', + endParagraph: '§ 11', + }), + Object.freeze({ + muster: /-v5$/, + nummer: 5, + url: '/abo-bedingungen.html', + pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', + endParagraph: '§ 11', + archiv: Object.freeze({ + url: '/vertragsunterlagen/v5/abo-bedingungen.html', + pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf', + }), + }), + Object.freeze({ + muster: /-v4$/, + nummer: 4, + url: '/vertragsunterlagen/v4/abo-bedingungen.html', + pdfFile: 'vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf', + endParagraph: '§ 11', + }), + Object.freeze({ + sonst: true, + nummer: 3, + url: '/vertragsunterlagen/v3/abo-bedingungen.html', + pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf', + endParagraph: '§ 9', + }), +]) + +/** Erster Tabelleneintrag mit passendem muster; ohne Treffer (auch bei Nicht-Text) der Eintrag sonst. */ +function fassungFuer(termsVersion) { + const treffer = typeof termsVersion === 'string' ? FASSUNGEN.find((f) => f.muster && f.muster.test(termsVersion)) : null + return treffer || FASSUNGEN.find((f) => f.sonst) +} + +/** + * Abo-Fassung der Sitzung: Fassung 6 nur bei neuer Ansicht (Pilot-Schalter), sonst die bisherige Fassung 5. + * req wie bei neueAnsicht(req) (req.session oder signiertes Cookie). + */ +export function aktuelleAboFassung(req) { + return neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion +} + /** Aktuelle Fassung 5 (Unterstuetzung in § 4 Abs. 4)? */ export function isCurrentTerms(termsVersion) { return typeof termsVersion === 'string' && termsVersion.endsWith('-v5') } -/** Fassung 4 oder neuer (Hosting- und Wartungs-Abo, Zusatzspeicher, Wechsel)? Aeltere und fehlende Fassungen: nein. */ +/** Fassung 4 oder neuer (Hosting- und Wartungs-Abo, Zusatzspeicher, Wechsel; v4, v5, v6)? Aeltere und fehlende Fassungen: nein. */ export function isTermsV4OrNewer(termsVersion) { - return typeof termsVersion === 'string' && /-v[45]$/.test(termsVersion) + return typeof termsVersion === 'string' && FASSUNGEN.some((f) => f.muster && f.muster.test(termsVersion)) } /** - * Vertragsunterlagen zur zugestimmten Fassung: Fassung 5 (Suffix "-v5") zeigt die aktuellen Bedingungen samt PDF, - * Fassung 4 das Archiv unter public/vertragsunterlagen/v4, jede aeltere Version (auch unbekannt/keine) das Archiv - * der Version 3. + * Vertragsunterlagen zur zugestimmten Fassung (Tabelle FASSUNGEN): Fassung 6 (Suffix "-v6") zeigt die neuen Bedingungen + * im Vorschaupfad, Fassung 5 die bisherigen Bedingungen samt PDF, Fassung 4 das Archiv unter public/vertragsunterlagen/v4, + * jede aeltere Version (auch unbekannt/keine) das Archiv der Version 3. * version: die Fassung, die eine Mail nennt - die gespeicherte, ohne gespeicherte die archivierte Fassung 3, * damit Versionsangabe und PDF-Anhang immer zusammenpassen (Task 12 Fix 1). * endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) in dieser Fassung, fuer die * Mails K02/K03/T05/T07 (F08) - ab Fassung 4 ist § 9 der Wechsel zwischen den Abos, § 11 das Vertragsende. + * Die Rueckgabe hat immer nur diese vier Felder (kein archiv, muster, nummer). */ export function termsDocFor(termsVersion) { - if (isCurrentTerms(termsVersion)) { - return { version: termsVersion, url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11' } - } - if (isTermsV4OrNewer(termsVersion)) { - return { - version: termsVersion, - url: '/vertragsunterlagen/v4/abo-bedingungen.html', - pdfFile: 'vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf', - endParagraph: '§ 11', - } - } - return { - version: typeof termsVersion === 'string' && termsVersion ? termsVersion : TERMS_VERSION_V3, - url: '/vertragsunterlagen/v3/abo-bedingungen.html', - pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf', - endParagraph: '§ 9', - } + const f = fassungFuer(termsVersion) + const version = typeof termsVersion === 'string' && termsVersion ? termsVersion : TERMS_VERSION_V3 + return { version, url: f.url, pdfFile: f.pdfFile, endParagraph: f.endParagraph } } /** Katalogname ohne "WEBklar " (z. B. "Hosting-Abo") fuer {{produkt}} in den Mails (F33); ohne Schluessel "Abo". */ diff --git a/test/products.test.js b/test/products.test.js index 0bf5124..823b165 100644 --- a/test/products.test.js +++ b/test/products.test.js @@ -14,6 +14,9 @@ const { productView, contractAttachmentFor, regularStorageLine, + FASSUNGEN, + aktuelleAboFassung, + isTermsV4OrNewer, } = await import('../server/services/products.js') const line = (price, amount, proration = false) => ({ @@ -136,6 +139,48 @@ test('Vertragsunterlagen je Version', () => { assert.equal(termsDocFor('2026-09-25-v2').version, '2026-09-25-v2', 'aeltere Fassung bleibt benannt') }) +test('Fassungstabelle: Reihenfolge, Muster nur am Ende, v6 im Vorschaupfad', () => { + assert.deepEqual(FASSUNGEN.map((f) => f.muster?.source ?? 'sonst'), ['-v6$', '-v5$', '-v4$', 'sonst']) + assert.deepEqual(termsDocFor('2026-10-16-v6'), { + version: '2026-10-16-v6', url: '/neu/rechtliches/abo-bedingungen.html', + pdfFile: 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11', + }) + for (const v of ['2026-10-16-v60', '2026-10-16-v6x', 'v6-2026-10-16', '2026-10-16-v66']) { + assert.equal(termsDocFor(v).url, '/vertragsunterlagen/v3/abo-bedingungen.html', v) + } + const a = contractAttachmentFor('2026-10-16-v6') + assert.equal(a.filename, 'WEBklar-Abo-Vertragsunterlagen.pdf') + assert.ok(path.isAbsolute(a.path) && a.path.endsWith(path.join('public', 'neu', 'rechtliches', 'WEBklar-Abo-Vertragsunterlagen.pdf'))) +}) +test('Fassung 5 bleibt bis P10 an den heutigen Adressen, das Archiv ist vorbereitet', () => { + const v5 = FASSUNGEN.find((f) => f.muster?.source === '-v5$') + assert.equal(termsDocFor('2026-09-30-v5').url, '/abo-bedingungen.html') + assert.deepEqual(Object.keys(termsDocFor('2026-09-30-v5')).sort(), ['endParagraph', 'pdfFile', 'url', 'version']) + assert.deepEqual(v5.archiv, { url: '/vertragsunterlagen/v5/abo-bedingungen.html', pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf' }) +}) +test('isTermsV4OrNewer: v4, v5, v6 ja; v3, fehlende und fremde Endungen nein', () => { + for (const v of ['2026-09-30-v4', '2026-09-30-v5', '2026-10-16-v6']) assert.equal(isTermsV4OrNewer(v), true, v) + for (const v of ['2026-09-25-v3', '2026-09-25', null, undefined, '', '2026-10-16-v60', 7]) assert.equal(isTermsV4OrNewer(v), false, String(v)) +}) +test('aktuelleAboFassung: v6 nur mit neuer Ansicht', () => { + // appwriteUserId gehört zur gültigen Kunden-Sitzung (N1, schalter.js istKundenSitzung) + const k = (email, extra = {}) => ({ session: { customerId: 'c1', appwriteUserId: 'u1', email, ...extra } }) + const vorher = { ...config.kundenansichtNeu } + try { + Object.assign(config.kundenansichtNeu, { mode: 'off', pilotEmails: ['*'] }) + assert.equal(aktuelleAboFassung(k('a@example.invalid')), '2026-09-30-v5', 'off schlägt die Liste') + Object.assign(config.kundenansichtNeu, { mode: 'pilot', pilotEmails: [] }) + assert.equal(aktuelleAboFassung(k('a@example.invalid')), '2026-09-30-v5', 'leere Liste lässt niemanden durch') + config.kundenansichtNeu.pilotEmails = ['a@example.invalid'] + assert.equal(aktuelleAboFassung(k(' A@Example.INVALID ')), '2026-10-16-v6', 'Großschreibung und Leerraum') + assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-09-30-v5') + assert.equal(aktuelleAboFassung(k('b@example.invalid', { viewAs: true, viewId: 'v1' })), '2026-10-16-v6', 'Mitarbeiter-Kundenansicht') + Object.assign(config.kundenansichtNeu, { mode: 'on', pilotEmails: [] }) + assert.equal(aktuelleAboFassung(k('b@example.invalid')), '2026-10-16-v6') + } finally { Object.assign(config.kundenansichtNeu, vorher) } + assert.deepEqual([config.abo.termsVersion, config.abo.termsVersionNeu], ['2026-09-30-v5', '2026-10-16-v6']) +}) + test('Produktansicht enthält Leistungen ohne 1-Postfach-Regel bei Wartung', () => { const w = productView('wartung').features.flat().join(' ') assert.ok(w.includes('beliebig viele Postfächer') && !w.includes('1 E-Mail-Postfach'))