diff --git a/server/routes/abo.js b/server/routes/abo.js index 2ce56d4..8292db3 100644 --- a/server/routes/abo.js +++ b/server/routes/abo.js @@ -24,7 +24,7 @@ import { assertOwnProject, handleFeatureCheckoutEvent, isFeatureCheckoutEvent, l import { featureConfig } from '../services/featureRegistry.js' import { istAboModell } from '../services/aboModell.js' import { ENTHALTEN_IN, aboModellFuerKunde } from '../services/aboFunktionen.js' -import { neueAnsicht } from '../services/kundenansicht/schalter.js' +import { neueAnsicht, neueAnsichtModus } from '../services/kundenansicht/schalter.js' import { FEHLER_BEDINGUNGEN, FEHLER_NACHWEIS, FEHLER_UNVOLLSTAENDIG, WECHSEL_GESPERRT, bestelltexteFuerClient, bestellZeile, nachweisTexte } from '../services/bestelltexte.js' import { kundenartWirksam } from '../services/kundenart.js' import { fassungDerAnfrage, istNeueFassung, speichereNachweisFuerRoute } from '../services/vertragsNachweis.js' @@ -117,7 +117,8 @@ function publicSubscription(sub, events = []) { if (until > new Date()) withdrawals.push({ type: 'abschluss', eventId: null, until }) } const withdrawalUntil = withdrawals.reduce((latest, w) => (!latest || w.until > latest ? w.until : latest), null) - const terms = termsDocFor(sub.termsVersion) + // Bei on liefert die Umschalt-Weiche (P10) unter /abo-bedingungen.html Fassung 6: wer Fassung 5 zugestimmt hat, bekommt das Archiv v5 + const terms = termsDocFor(sub.termsVersion, { archiv: neueAnsichtModus() === 'on' }) return { status: sub.status, active, diff --git a/server/routes/umschaltSeiten.js b/server/routes/umschaltSeiten.js index d6f5302..c98db0e 100644 --- a/server/routes/umschaltSeiten.js +++ b/server/routes/umschaltSeiten.js @@ -18,6 +18,7 @@ export const UMSCHALT_SEITEN = Object.freeze({ '/login.html': 'neu/oeffentlich/login.html', '/kuendigen.html': 'neu/oeffentlich/kuendigen.html', '/widerrufen.html': 'neu/oeffentlich/widerrufen.html', + '/auftrag.html': 'neu/oeffentlich/auftrag.html', // Link der Auftragsbestaetigung (/auftrag.html#t=), Uebersicht N40 '/offline.html': 'neu/oeffentlich/offline.html', '/manifest.webmanifest': 'neu/manifest.webmanifest', }) diff --git a/server/services/products.js b/server/services/products.js index d2ba9c6..cb7e9fe 100644 --- a/server/services/products.js +++ b/server/services/products.js @@ -33,7 +33,7 @@ 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). + * Adressen, archiv nennt die Adressen der Kopie unter public/vertragsunterlagen/v5/ (P10 schaltet sie bei on ein, siehe termsDocFor). * endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) fuer die Mails K02/K03/T05/T07 (F08). */ export const FASSUNGEN = Object.freeze([ @@ -103,12 +103,16 @@ export function isTermsV4OrNewer(termsVersion) { * 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. + * archiv: true nennt statt der heutigen Adressen die Kopie unter public/vertragsunterlagen/v5/, wo die Tabelle ein Feld + * archiv hat (nur Fassung 5). Das braucht die Anzeige im Portal, sobald die Umschalt-Weiche (P10) unter + * /abo-bedingungen.html Fassung 6 liefert; Mail-Anhaenge rufen ohne archiv auf (gleiche PDF-Bytes). * Die Rueckgabe hat immer nur diese vier Felder (kein archiv, muster, nummer). */ -export function termsDocFor(termsVersion) { +export function termsDocFor(termsVersion, { archiv = false } = {}) { 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 } + const doc = archiv && f.archiv ? f.archiv : f + return { version, url: doc.url, pdfFile: doc.pdfFile, endParagraph: f.endParagraph } } /** Katalogname ohne "WEBklar " (z. B. "Hosting-Abo") fuer {{produkt}} in den Mails (F33); ohne Schluessel "Abo". */ diff --git a/test/aboRoutes.test.js b/test/aboRoutes.test.js index 54abb6c..4be414b 100644 --- a/test/aboRoutes.test.js +++ b/test/aboRoutes.test.js @@ -29,6 +29,7 @@ const { withdrawalDeadline } = await import('../server/services/deadlines.js') const { offerText } = await import('../server/services/offers.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { mailSink } = await import('./support/mailSink.js') +const { termsDocFor } = await import('../server/services/products.js') const app = await startTestApp({ '/api/abo': aboRoutes }) // F25: Mails landen in einer Senke statt bei SMTP, damit K01 (Anhang je Fassung) und T02 pruefbar sind (Task 12) @@ -547,3 +548,30 @@ test('Webhook: Nachweis voruebergehend nicht speicherbar (500) → 500, Wiederho await cleanupWebhook(subId, 'o3') } }) + +// P10 (Prüfung Task 3): Bei on liefert die Weiche unter /abo-bedingungen.html und /WEBklar-Abo-Vertragsunterlagen.pdf +// Fassung 6. Wer Fassung 5 zugestimmt hat, bekommt in Verträge und Stunden deshalb das Archiv v5 (Altverträge bleiben auffindbar). +test('GET /api/abo: bei on führen Bedingungen und PDF einer Fassung-5-Zustimmung ins Archiv v5, sonst bleiben die Adressen', async () => { + const gemerkt = { ...config.kundenansichtNeu } + const adressen = async (termsVersion) => { + aw.seed('subscriptions', subscriptionDoc('sub_h', { ...hostingDoc, termsVersion })) + const r = await get('/api/abo', { cookie: sessionCookie({ customerId: 'c1', appwriteUserId: 'u1' }) }) + return [r.subscription.termsUrl, r.subscription.termsPdfUrl] + } + const heute = (v) => [termsDocFor(v).url, `/${termsDocFor(v).pdfFile}`] + const V5 = '2026-09-30-v5' + const ARCHIV_V5 = ['/vertragsunterlagen/v5/abo-bedingungen.html', '/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf'] + try { + for (const mode of ['off', 'pilot']) { + Object.assign(config.kundenansichtNeu, { mode, pilotEmails: ['*'] }) + assert.deepEqual(await adressen(V5), heute(V5), mode) + } + Object.assign(config.kundenansichtNeu, { mode: 'on', pilotEmails: [] }) + assert.deepEqual(await adressen(V5), ARCHIV_V5) + // alle anderen Fassungen behalten ihre Adressen (v6 zeigt auf Fassung 6, v4 und v3 liegen schon im Archiv) + for (const v of [config.abo.termsVersionNeu, '2026-09-30-v4', '2026-09-25-v3']) assert.deepEqual(await adressen(v), heute(v), v) + } finally { + Object.assign(config.kundenansichtNeu, gemerkt) + aw.seed('subscriptions', hostingDoc) + } +}) diff --git a/test/knUmschaltSeiten.test.js b/test/knUmschaltSeiten.test.js index c7c0619..90819c6 100644 --- a/test/knUmschaltSeiten.test.js +++ b/test/knUmschaltSeiten.test.js @@ -16,6 +16,8 @@ const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { startTestApp } = await import('./support/testApp.js') const { umschaltSeitenRouter, UMSCHALT_SEITEN, fassung6Aus } = await import('../server/routes/umschaltSeiten.js') +const { config } = await import('../server/config.js') +const { termsDocFor } = await import('../server/services/products.js') const publicDir = fileURLToPath(new URL('../public/', import.meta.url)) let modus = 'off' @@ -32,6 +34,10 @@ const holen = async (pfad, basis = app.url, methode = 'GET') => { } const datei = (rel) => readFileSync(path.join(publicDir, rel)) +test('Übersicht N40: auch die öffentliche Auftragsseite (/auftrag.html#t= aus der Mail K31) ist eine Zeile der Weiche', () => { + assert.equal(UMSCHALT_SEITEN['/auftrag.html'], 'neu/oeffentlich/auftrag.html') +}) + for (const [pfad, neu] of Object.entries(UMSCHALT_SEITEN)) { test(`${pfad}: off und pilot liefern die Datei von heute, on die neue (no-cache)`, async () => { for (const m of ['off', 'pilot']) { @@ -51,6 +57,7 @@ test('Manifest bei on ist JSON', async () => { assert.match((await holen('/manifest.webmanifest')).headers.get('content-type'), /json/) }) +// P10 Task 14: entfällt (Parameter fassung6 verschwindet, Fassung 6 liegt dann unter /abo-bedingungen.html) test('Fassung 6: Seite und PDF kommen bei on aus der Neu-Fassung, sonst bleibt die Datei von heute', async () => { fassung6 = { url: '/neu/oeffentlich/offline.html', pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf' } // Stellvertreter-Dateien modus = 'pilot' @@ -62,6 +69,7 @@ test('Fassung 6: Seite und PDF kommen bei on aus der Neu-Fassung, sonst bleibt d assert.ok((await holen('/abo-bedingungen.html')).bytes.equals(datei('abo-bedingungen.html'))) }) +// P10 Task 14: entfällt (fassung6Aus verschwindet, public/neu/rechtliches/ wird zu Weiterleitungen) test('Standard-Fassung 6 (fassung6Aus): bei on kommen Seite und PDF der Fassung 6 aus public/neu/rechtliches/', async () => { const f6 = fassung6Aus() assert.deepEqual([f6.url, f6.pdfFile], ['/neu/rechtliches/abo-bedingungen.html', 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf']) @@ -80,6 +88,7 @@ test('Standard-Fassung 6 (fassung6Aus): bei on kommen Seite und PDF der Fassung } }) +// P10 Task 14: fällt mit dem Parameter fassung6 weg oder prüft dann die Ziele der Weiterleitungen test('Pfadwächter: eine Adresse außerhalb von public/ wird nie geliefert', async () => { modus = 'on' fassung6 = { url: '/../../package.json', pdfFile: '../../server/config.js' } @@ -122,6 +131,36 @@ test('Groß- und Kleinschreibung: /Login.html wird nicht umgeleitet (wie express assert.equal((await holen('/Login.html')).status, 404) }) +test('echter Schalter: der Router mit den Vorgaben (wie in server/index.js) folgt config.kundenansichtNeu, nur on ändert Seiten', async () => { + const standard = express.Router() + standard.use(umschaltSeitenRouter({ publicDir })) // ohne modus und fassung6: genau der Aufruf in server/index.js + standard.use(express.static(publicDir)) + const echt = await startTestApp({ '/': standard }) + const gemerkt = { ...config.kundenansichtNeu } + const aboNeu = datei(termsDocFor(config.abo.termsVersionNeu).url.slice(1)) + const faelle = [ // [mode, pilotEmails, liefert die Weiche die neuen Seiten?] + ['off', [], false], ['off', ['*'], false], + ['pilot', [], false], ['pilot', ['*'], false], ['pilot', ['kunde@example.invalid'], false], + ['an', ['*'], false], ['', ['*'], false], [undefined, ['*'], false], + ['on', [], true], ['on', ['*'], true], + ] + try { + for (const [mode, pilotEmails, neu] of faelle) { + Object.assign(config.kundenansichtNeu, { mode, pilotEmails }) + const fall = `${mode} [${pilotEmails}]` + for (const [pfad, rel] of Object.entries(UMSCHALT_SEITEN)) { + const r = await holen(pfad, echt.url) + assert.ok(r.status === 200 && r.bytes.equals(datei(neu ? rel : pfad.slice(1))), `${fall} ${pfad}`) + } + const abo = await holen('/abo-bedingungen.html', echt.url) + assert.ok(abo.status === 200 && abo.bytes.equals(neu ? aboNeu : datei('abo-bedingungen.html')), `${fall} /abo-bedingungen.html`) + } + } finally { + Object.assign(config.kundenansichtNeu, gemerkt) + await echt.close() + } +}) + test('server/index.js hängt die Weiche hinter P0s Weichen und vor express.static ein', () => { const index = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8') const reihe = ["app.get('/dashboard.html', dashboardWeiche(", 'app.use(umschaltSeitenRouter(', 'app.use(express.static(publicDir))'].map((s) => index.indexOf(s)) diff --git a/test/products.test.js b/test/products.test.js index 89d9c73..347a5fc 100644 --- a/test/products.test.js +++ b/test/products.test.js @@ -158,6 +158,16 @@ test('Fassung 5 bleibt bis P10 an den heutigen Adressen, das Archiv ist vorberei 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('termsDocFor mit archiv: nur Fassung 5 wechselt in das Archiv v5, die Form bleibt (P10: Weiche liefert bei on Fassung 6)', () => { + assert.deepEqual(termsDocFor('2026-09-30-v5', { archiv: true }), { + version: '2026-09-30-v5', + url: '/vertragsunterlagen/v5/abo-bedingungen.html', + pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf', + endParagraph: '§ 11', + }) + assert.deepEqual(termsDocFor('2026-09-30-v5', { archiv: false }), termsDocFor('2026-09-30-v5')) + for (const v of ['2026-10-28-v6', '2026-09-30-v4', '2026-09-25-v3', null, undefined, '']) assert.deepEqual(termsDocFor(v, { archiv: true }), termsDocFor(v), String(v)) +}) 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-28-v6']) assert.equal(isTermsV4OrNewer(v), true, v) for (const v of ['2026-09-25-v3', '2026-09-25', null, undefined, '', '2026-10-28-v60', 7]) assert.equal(isTermsV4OrNewer(v), false, String(v))