Abo-Fassung 6: Wortlaute für Linkzeile und Häkchen, Unterlagen-Tabelle der Funktionen
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a8db582cb
commit
214d698d6f
2 files changed
+112
No files matched your search
@@ -214,3 +214,65 @@ export function creditMinutesForInvoice(invoice, priceKeys) {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
* Fassung 6 (Entwurf bis zur Freigabe, E20): Wortlaute fuer die Bestellseiten und Einschalt-Seiten
|
||||
* sowie die Unterlagen-Tabelle der Funktionen. Dieses Paket liefert die Konstanten; verdrahtet werden
|
||||
* sie erst mit P5/P7 (Nachtrag N33). Texte in "du"-Form, ohne Preise.
|
||||
* ------------------------------------------------------------------------------------------- */
|
||||
|
||||
/** Fassung der Funktionsunterlagen (Leistungsbeschreibungen, AVV). Entwurfstag; bei der Freigabe das Datum setzen. */
|
||||
export const FUNKTIONEN_FASSUNG_V6 = '2026-10-16'
|
||||
|
||||
/**
|
||||
* Wortlaute der Oberflaeche. linkzeile: genau ein Platzhalter {fassung} (mitFassung fuellt ihn);
|
||||
* bestellen gilt fuer Abo, Wechsel und Zusatzspeicher. beginn: das Haekchen nur fuer Privatkunden in der Frist.
|
||||
* hinweis: die Pflichten der alten Haekchen verantwortung, stripe, gesundheit als ein Satz auf der Einschalt-Seite.
|
||||
*/
|
||||
export const WORTLAUT_V6 = Object.freeze({
|
||||
linkzeile: Object.freeze({
|
||||
bestellen: 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung {fassung}). Alles kommt danach auch als PDF per Mail.',
|
||||
einschaltenMitAvv: 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung {fassung}).',
|
||||
einschaltenOhneAvv: 'Es gilt die Leistungsbeschreibung (Fassung {fassung}).',
|
||||
}),
|
||||
beginn: Object.freeze({
|
||||
abo: 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
|
||||
wechsel: 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
|
||||
einrichtung: 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.',
|
||||
}),
|
||||
hinweis: Object.freeze({
|
||||
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
|
||||
unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
|
||||
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
|
||||
}),
|
||||
})
|
||||
|
||||
/** Ersetzt {fassung} im Wortlaut; wirft bei leerer Fassung oder wenn der Text keinen Platzhalter enthaelt. */
|
||||
export function mitFassung(text, fassung) {
|
||||
if (typeof fassung !== 'string' || fassung.trim() === '') throw new Error('Fassung fehlt oder ist leer')
|
||||
if (typeof text !== 'string' || !text.includes('{fassung}')) throw new Error('Platzhalter {fassung} fehlt im Text')
|
||||
return text.split('{fassung}').join(fassung)
|
||||
}
|
||||
|
||||
/** Ein Dokument der Funktionsunterlagen im Vorschaupfad: Seite, PDF und Dateiname des Mail-Anhangs. */
|
||||
function dok(slug, anhang) {
|
||||
return Object.freeze({
|
||||
version: FUNKTIONEN_FASSUNG_V6,
|
||||
url: `/neu/rechtliches/${slug}-${FUNKTIONEN_FASSUNG_V6}.html`,
|
||||
pdfFile: `neu/rechtliches/${slug}-${FUNKTIONEN_FASSUNG_V6}.pdf`,
|
||||
anhangName: `${anhang}-${FUNKTIONEN_FASSUNG_V6}.pdf`,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unterlagen je Funktion in Fassung 6: { [key]: { lb, avv } }. avv ist null, wo keine personenbezogenen Daten im
|
||||
* Auftrag verarbeitet werden (News, Oeffnungszeiten live). Reihenfolge: news, termine, shop, unterkunft, zeiten, analyse.
|
||||
*/
|
||||
export const FUNKTIONEN_UNTERLAGEN_V6 = Object.freeze({
|
||||
news: Object.freeze({ lb: dok('leistungsbeschreibung-news', 'Leistungsbeschreibung-News-Pinnwand'), avv: null }),
|
||||
termine: Object.freeze({ lb: dok('leistungsbeschreibung-terminplaner', 'Leistungsbeschreibung-Terminplaner'), avv: dok('avv-terminplaner', 'AVV-Terminplaner') }),
|
||||
shop: Object.freeze({ lb: dok('leistungsbeschreibung-webshop', 'Leistungsbeschreibung-Webshop'), avv: dok('avv-webshop', 'AVV-Webshop') }),
|
||||
unterkunft: Object.freeze({ lb: dok('leistungsbeschreibung-unterkunft', 'Leistungsbeschreibung-Buchungskalender'), avv: dok('avv-unterkunft', 'AVV-Buchungskalender') }),
|
||||
zeiten: Object.freeze({ lb: dok('leistungsbeschreibung-zeiten', 'Leistungsbeschreibung-Oeffnungszeiten-live'), avv: null }),
|
||||
analyse: Object.freeze({ lb: dok('leistungsbeschreibung-website-analyse', 'Leistungsbeschreibung-Website-Analyse'), avv: dok('avv-website-analyse', 'AVV-Website-Analyse') }),
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { WORTLAUT_V6, FUNKTIONEN_FASSUNG_V6, FUNKTIONEN_UNTERLAGEN_V6, mitFassung } = await import('../server/services/products.js')
|
||||
|
||||
test('Linkzeilen: Wortlaut, Platzhalter wird gefüllt', () => {
|
||||
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.bestellen, '2026-10-16-v6'), 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung 2026-10-16-v6). Alles kommt danach auch als PDF per Mail.')
|
||||
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, '2026-10-16'), 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16).')
|
||||
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, '2026-10-16'), 'Es gilt die Leistungsbeschreibung (Fassung 2026-10-16).')
|
||||
assert.throws(() => mitFassung(WORTLAUT_V6.linkzeile.bestellen, ''), /Fassung/)
|
||||
assert.throws(() => mitFassung('ohne Platzhalter', '2026-10-16'), /Platzhalter/)
|
||||
})
|
||||
test('Häkchen beginn: Wortlaut je Fall (nur privat in der Frist)', () => {
|
||||
assert.deepEqual(WORTLAUT_V6.beginn, {
|
||||
abo: 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
|
||||
wechsel: 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
|
||||
einrichtung: 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.',
|
||||
})
|
||||
})
|
||||
test('Pflichten aus den alten Häkchen verantwortung, stripe, gesundheit', () => {
|
||||
assert.deepEqual(WORTLAUT_V6.hinweis, {
|
||||
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
|
||||
unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
|
||||
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
|
||||
})
|
||||
})
|
||||
test('alle Wortlaute: du-Form, kein Preis, nur {fassung} als Platzhalter, tief eingefroren', () => {
|
||||
const alle = JSON.stringify(WORTLAUT_V6)
|
||||
assert.doesNotMatch(alle, /€|\bSie\b|\bIhr(e[nmrs]?)?\b/)
|
||||
assert.deepEqual([...alle.matchAll(/\{[a-z]+\}/g)].map((m) => m[0]).filter((x) => x !== '{fassung}'), [])
|
||||
for (const t of Object.values(WORTLAUT_V6.linkzeile)) assert.equal((t.match(/\{fassung\}/g) ?? []).length, 1)
|
||||
for (const o of [WORTLAUT_V6, WORTLAUT_V6.linkzeile, WORTLAUT_V6.beginn, WORTLAUT_V6.hinweis]) assert.ok(Object.isFrozen(o))
|
||||
})
|
||||
test('Funktionsunterlagen Fassung 6: Reihenfolge, Vorschaupfad, AVV nur wo Daten im Auftrag verarbeitet werden', () => {
|
||||
assert.equal(FUNKTIONEN_FASSUNG_V6, '2026-10-16')
|
||||
assert.deepEqual(Object.keys(FUNKTIONEN_UNTERLAGEN_V6), ['news', 'termine', 'shop', 'unterkunft', 'zeiten', 'analyse'])
|
||||
assert.deepEqual(FUNKTIONEN_UNTERLAGEN_V6.termine.lb, { version: '2026-10-16', url: '/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html', pdfFile: 'neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.pdf', anhangName: 'Leistungsbeschreibung-Terminplaner-2026-10-16.pdf' })
|
||||
const slugs = Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.url.replace(/^\/neu\/rechtliches\/|-2026-10-16\.html$/g, ''))
|
||||
assert.deepEqual(slugs, ['leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'])
|
||||
assert.deepEqual([FUNKTIONEN_UNTERLAGEN_V6.news.avv, FUNKTIONEN_UNTERLAGEN_V6.zeiten.avv], [null, null])
|
||||
assert.deepEqual(Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.anhangName.replace('-2026-10-16.pdf', '')), ['Leistungsbeschreibung-News-Pinnwand', 'Leistungsbeschreibung-Terminplaner', 'AVV-Terminplaner', 'Leistungsbeschreibung-Webshop', 'AVV-Webshop', 'Leistungsbeschreibung-Buchungskalender', 'AVV-Buchungskalender', 'Leistungsbeschreibung-Oeffnungszeiten-live', 'Leistungsbeschreibung-Website-Analyse', 'AVV-Website-Analyse'])
|
||||
})
|
||||
test('Verdrahtung mit P5 (läuft erst nach dem Merge von kn/server; rot, bis der Controller verdrahtet hat)', { skip: config.aboFunktionen === undefined && 'config.aboFunktionen fehlt: P5 noch nicht gemerged' }, async () => {
|
||||
const reg = await import('../server/services/featureRegistry.js')
|
||||
assert.equal(config.aboFunktionen.fassung, FUNKTIONEN_FASSUNG_V6)
|
||||
assert.equal(config.aboFunktionen.texte.beginn, WORTLAUT_V6.beginn.einrichtung)
|
||||
for (const key of ['termine', 'shop', 'unterkunft']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, FUNKTIONEN_FASSUNG_V6), key)
|
||||
for (const key of ['news', 'zeiten']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, FUNKTIONEN_FASSUNG_V6), key)
|
||||
})
|
||||
Reference in new issue
Block a user