Webshop A3: Register-Eintrag shop, Haken, Rechtstext-Satz und Ticket-Details

Registereintrag config.features.shop (Bestelltexte mit Fassung config.shop.fassung, Rechtsunterlagen-Getter,
Mailtexte, Ticket), STANDARD_HAKEN.shop (Schalter, Bestellpruefung ueber shop/anfrage.js, Unterlagen, Belehrung),
legalDocs-Satz shop, shopTicketDetails sowie Rumpf von anfrageOffenePunkte. Tests angepasst und um Shop-Faelle erweitert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 08:35:28 +00:00
1 parent 6782d99960
commit 0e74f1669c
12 files changed
+594 -23

No files matched your search

+38
View File
@@ -149,6 +149,44 @@ test('textstand_pin: Hash über die verbundenen Bestelltexte beider Funktionen s
assert.equal(sha256(texte), TEXTSTAND.sha256, 'Bestelltexte geändert: neues Fassungsdatum und neuen Hash eintragen')
})
/**
* Pin auf die Bestelltexte des Webshops (Haken und Knopf, durch eine Leerzeile verbunden), gebunden an config.shop.fassung.
* Bei Textänderung neues Fassungsdatum (config.shop.fassung) UND neuen Hash: gespeicherte Nachweise tragen Text und Fassung.
*/
const SHOP_TEXTSTAND = { fassung: '2026-10-05', sha256: '09553de56c794602ca462d980de8548015df7b03d90a5ba1227ce83a58a6608a' }
test('shop_haken_woertlich: fünf Haken in fester Reihenfolge mit dem Wortlaut aus dem Plan', () => {
const S = config.shop.fassung
const SHOP = config.features.shop
assert.deepEqual(SHOP.consent.items.map((i) => i.id), ['unterlagen', 'verantwortung', 'stripe', 'beginn', 'information'])
assert.deepEqual(SHOP.consent.items.map((i) => i.haken), [true, true, true, true, true])
assert.deepEqual(SHOP.consent.items.map((i) => i.text), [
`Ich stimme der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop in der Fassung ${S} zu.`,
'Ich verkaufe als Unternehmer in eigenem Namen. Für meine Produkte, Preise, Produktangaben und Rechtstexte (AGB, Widerrufsbelehrung, Impressum, Datenschutzerklärung) bin ich selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Ich verkaufe keine der in der Leistungsbeschreibung ausgeschlossenen Waren.',
'Die Zahlungen meiner Käufer laufen über mein eigenes Stripe-Konto zu den Bedingungen von Stripe. WEBklar erhält und verwahrt kein Geld meiner Käufer und erhebt keine Umsatzbeteiligung.',
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Einrichtung des Webshops beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Einrichtung erlischt (§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).',
'Ich habe die Pflichtinformationen zu dieser Bestellung gelesen: Leistung (Einrichtung des Webshops für diese Website: Einbau des Shop-Widgets, Verwaltung im Portal, Muster-Rechtstexte), Gesamtpreis 299 € einmalig (ohne Umsatzsteuer gemäß § 19 UStG), voraussichtliche Dauer meist innerhalb von 2–3 Wochen, keine laufende Gebühr von WEBklar; der Webshop läuft, solange WEBklar die Website betreibt. Die Widerrufsbelehrung mit Muster-Widerrufsformular habe ich gelesen.',
])
assert.equal(SHOP.consent.knopf, 'Zahlungspflichtig bestellen · 299 €')
for (const item of SHOP.consent.items) assert.deepEqual(Object.keys(item).sort(), ['haken', 'id', 'text'], 'ohne url')
assert.equal(SHOP.consent.version, S)
})
test('shop_textstand_pin: Hash über die verbundenen Bestelltexte des Webshops steht im Test', () => {
const SHOP = config.features.shop
const texte = [...SHOP.consent.items.map((i) => i.text), SHOP.consent.knopf].join('\n\n')
assert.equal(config.shop.fassung, SHOP_TEXTSTAND.fassung, 'neue Fassung: Pin und Hash hier und in den Rechtstext-Tests nachziehen')
assert.equal(sha256(texte), SHOP_TEXTSTAND.sha256, 'Bestelltexte geändert: neues Fassungsdatum und neuen Hash eintragen')
})
test('shop_belehrung_haken: dieselbe Belehrung wie beim Terminplaner, ohne ENTWURF', async () => {
const belehrung = await featureHaken('shop').belehrung()
assert.equal(belehrung, widerrufsbelehrungFunktion())
assert.equal(belehrung, await featureHaken('termine').belehrung())
assert.deepEqual(config.features.shop.widerruf, { version: config.shop.fassung, url: legalPaths('shop', config.shop.fassung).widerruf })
assert.doesNotMatch(JSON.stringify([config.features.shop.consent.items, belehrung]), /ENTWURF/)
})
// ------------------------------------------------------------------------------------------------ Belehrung im Haken
test('belehrung_haken: der Standard-Haken liefert die Widerrufsbelehrung der Funktionen, News hat keinen', async () => {
+4 -4
View File
@@ -103,15 +103,15 @@ afterEach(() => {
// ---------------------------------------------------------------------------------------- Register ---
test('ticketFreischaltungKeys: Register-Schluessel mit ticketFreischaltung, in Registerreihenfolge', () => {
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine'])
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'shop'])
NEWS.ticketFreischaltung = false
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine'])
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine', 'shop'])
NEWS.ticketFreischaltung = 'ja'
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine'], 'nur true zaehlt')
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine', 'shop'], 'nur true zaehlt')
NEWS.ticketFreischaltung = true
TERMINE.ticketFreischaltung = false
try {
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news'])
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'shop'])
} finally {
TERMINE.ticketFreischaltung = true
}
+207 -3
View File
@@ -4,7 +4,7 @@ import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// Register config.features (News + Terminplaner) und featureRegistry.js. Dienste (siteFeatures, ticketAdmin)
// Register config.features (News, Terminplaner, Webshop) und featureRegistry.js. Dienste (siteFeatures, ticketAdmin)
// brauchen die Test-Umgebung vor dem ersten Import von server/config.js.
const aw = await startAppwriteMock()
const st = await startFakeStripe()
@@ -121,10 +121,11 @@ test('FEATURE_LABELS und FEATURE_KEYS unverändert, aus dem Register abgeleitet'
assert.deepEqual(sf.FEATURE_LABELS, {
news: { name: 'News-Pinnwand', keine: 'keine News-Pinnwand' },
termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' },
shop: { name: 'Webshop', keine: 'keinen Webshop' },
})
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine'])
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'shop'])
assert.deepEqual(sf.FEATURE_KEYS, reg.featureKeys())
assert.deepEqual(reg.featureKeys(), ['news', 'termine'])
assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'shop'])
})
// ---------------------------------------------------------------------------------- Schlüsselschutz ---
@@ -295,3 +296,206 @@ test('Bestelltexte ohne ENTWURF', async () => {
const belehrung = reg.featureHaken('termine').belehrung
if (belehrung) frei(await belehrung(), 'belehrung')
})
// ---------------------------------------------------------------------------------- Webshop (Block A3) ---
test('Shop: Preis, Struktur und Texte', async () => {
const s = reg.featureConfig('shop')
assert.equal(s, config.features.shop)
assert.equal(s.key, 'shop')
assert.equal(s.product, 'shopSetup')
assert.equal(config.shopSetup.amountCents, 29900)
assert.deepEqual(s.label, { name: 'Webshop', kurz: 'Shop', keine: 'keinen Webshop' })
assert.equal(s.priceLabel, '299 €')
assert.equal(s.setupEstimate, 'meist innerhalb von 2–3 Wochen')
assert.equal(s.consent.knopf, 'Zahlungspflichtig bestellen · 299 €')
assert.deepEqual(
s.consent.items.map((i) => i.id),
['unterlagen', 'verantwortung', 'stripe', 'beginn', 'information']
)
assert.deepEqual(
s.consent.items.map((i) => i.haken),
[true, true, true, true, true]
)
for (const item of s.consent.items) {
assert.equal(typeof item.text, 'string')
assert.ok(item.text.length > 0)
assert.equal(item.url, undefined, 'ohne url')
}
const F = config.shop.fassung
assert.equal(s.consent.version, F)
assert.equal(s.consent.items[0].text, `Ich stimme der Leistungsbeschreibung Webshop und dem Auftragsverarbeitungsvertrag Webshop in der Fassung ${F} zu.`)
assert.match(s.consent.items[3].text, /Einrichtung des Webshops/)
assert.ok(s.consent.items[4].text.includes(`Gesamtpreis 299 € einmalig (${config.ustHinweis})`))
assert.ok(s.consent.items[4].text.includes('meist innerhalb von 2–3 Wochen'))
// Fassung und Adressen folgen config.shop.fassung; die Widerrufsbelehrung ist die der Funktionen
assert.deepEqual(s.leistungsbeschreibung, { version: F, url: `/rechtliches/leistungsbeschreibung-webshop-${F}.html` })
assert.deepEqual(s.avv, { version: F, url: `/rechtliches/avv-webshop-${F}.html` })
assert.deepEqual(s.widerruf, { version: F, url: `/rechtliches/widerrufsbelehrung-funktionen-${F}.html` })
assert.equal(s.returnHash, 'webshop')
assert.equal(s.returnProjekt, true)
assert.equal(s.requiresWartungsAbo, false)
assert.equal(s.ticketFreischaltung, true)
assert.equal(s.disabledMessage, 'Der Webshop ist für diese Website abgeschaltet. Schreib uns im Chat, wenn Du ihn wieder möchtest.')
assert.equal(s.orderedConflictMessage, 'Der Webshop ist für dieses Projekt schon bestellt.')
assert.equal(s.unavailableMessage, 'Der Webshop ist noch nicht verfügbar.')
assert.equal(s.mails.funktion, 'Webshop')
assert.deepEqual(s.mails.formen, { dein: 'Dein Webshop', nom: 'der Webshop', akk: 'den Webshop', gen: 'des Webshops', pron: 'ihn' })
assert.equal(s.mails.knopf, 'Webshop öffnen')
assert.equal(s.mails.anleitung, false)
assert.equal(s.mails.preisHinweis, `einmalig, ${config.ustHinweis}; keine laufende Gebühr von WEBklar, Stripe-Gebühren zahlst Du direkt an Stripe`)
assert.equal(s.mails.danach, 'Danach kannst Du im Portal Artikel anlegen, Dein Stripe-Konto verbinden und den Shop öffnen.')
assert.equal(s.mails.bereit, 'Dein Webshop ist eingebaut. Sobald Stripe-Konto, Artikel und Rechtstexte fertig sind, kannst Du ihn im Portal öffnen.')
assert.equal(s.ticket.topic({ projectName: 'Laden' }), 'Webshop einbauen – Laden')
assert.equal(s.ticket.topic({ subdomain: 'laden' }), 'Webshop einbauen – laden')
assert.equal(typeof s.ticket.details, 'function')
const ansicht = reg.oeffentlicheAnsicht('shop')
assert.equal(ansicht.name, 'Webshop')
assert.equal(ansicht.kurz, 'Shop')
assert.equal(ansicht.priceLabel, '299 €')
assert.equal(ansicht.requiresWartungsAbo, false)
assert.equal(ansicht.returnHash, 'webshop')
assert.deepEqual(ansicht.consent.items.map((i) => i.id), ['unterlagen', 'verantwortung', 'stripe', 'beginn', 'information'])
assert.equal(ansicht.consent.version, F)
assert.deepEqual(ansicht.avv, { version: F, url: `/rechtliches/avv-webshop-${F}.html` })
assert.doesNotMatch(JSON.stringify(ansicht), /ENTWURF/)
// Termine bleibt unberührt
assert.equal(config.features.termine.consent.knopf, 'Zahlungspflichtig bestellen · 149 €')
})
test('Shop: Fassung folgt config.shop.fassung bei jedem Zugriff', () => {
const s = config.features.shop
const vorher = config.shop.fassung
try {
config.shop.fassung = '2099-01-02'
assert.equal(s.consent.version, '2099-01-02')
assert.ok(s.consent.items[0].text.includes('in der Fassung 2099-01-02 zu.'))
assert.deepEqual(s.avv, { version: '2099-01-02', url: '/rechtliches/avv-webshop-2099-01-02.html' })
assert.equal(s.leistungsbeschreibung.url, '/rechtliches/leistungsbeschreibung-webshop-2099-01-02.html')
// null beim Widerruf heißt "keine Unterlage" (Tests setzen so eine Bestellung ohne sie), undefined stellt den Standard her
s.widerruf = null
assert.equal(s.widerruf, null)
s.widerruf = undefined
assert.equal(s.widerruf.version, '2099-01-02')
} finally {
config.shop.fassung = vorher
s.widerruf = undefined
}
assert.equal(s.consent.version, vorher)
assert.ok(s.consent.items[0].text.includes(`in der Fassung ${vorher} zu.`))
})
test('Shop-Haken: verfügbar nach Schalter, Belehrung, kein freischaltbar', async () => {
const haken = reg.featureHaken('shop')
assert.deepEqual(Object.keys(haken).sort(), ['belehrung', 'bestellpruefung', 'unterlagenBereit', 'verfuegbar'])
assert.equal(haken.freischaltbar, undefined)
const vorher = { ...config.shop }
try {
config.shop.mode = 'off'
assert.equal(haken.verfuegbar('a@b.de'), false)
assert.equal(reg.featureSperre('shop', 'a@b.de'), 'Der Webshop ist noch nicht verfügbar.')
config.shop.mode = 'pilot'
config.shop.pilotEmails = ['a@b.de']
assert.equal(haken.verfuegbar('A@B.de'), true)
assert.equal(haken.verfuegbar('x@y.de'), false)
assert.equal(reg.featureSperre('shop', 'a@b.de'), null)
config.shop.mode = 'on'
assert.equal(haken.verfuegbar('x@y.de'), true)
} finally {
Object.assign(config.shop, vorher)
}
// dieselbe Belehrung wie beim Terminplaner (gleiche Quelle)
assert.equal(await haken.belehrung(), await reg.featureHaken('termine').belehrung())
assert.equal(typeof (await haken.belehrung()), 'string')
})
test('Shop-Haken: Bestellprüfung verlangt die angenommene Anfrage', async () => {
const { bestellpruefung } = reg.featureHaken('shop')
const OFFEN = ['Deine Webshop-Anfrage ist noch nicht freigegeben.']
aw.seed('customers', { $id: 'sc1', name: 'Shop Eins' })
aw.seed('websiteProjects', { $id: 'sp1', customerId: 'sc1', projectName: 'Laden' })
// kein portalFeatures-Dokument
assert.deepEqual(await bestellpruefung({ customerId: 'sc1', projectId: 'sp1' }), OFFEN)
aw.seed('portalFeatures', { $id: 'f_sp1_shop', customerId: 'sc1', projectId: 'sp1', featureKey: 'shop', siteKey: SITE_KEY, state: 'off', enabled: false })
// portalFeatures-Dokument, aber kein shopConfig
assert.deepEqual(await bestellpruefung({ customerId: 'sc1', projectId: 'sp1' }), OFFEN)
for (const status of ['', 'offen', 'abgelehnt', 'zurueckgezogen']) {
aw.seed('shopConfig', { $id: 'f_sp1_shop', customerId: 'sc1', projectId: 'sp1', siteKey: SITE_KEY, anfrageStatus: status })
assert.deepEqual(await bestellpruefung({ customerId: 'sc1', projectId: 'sp1' }), OFFEN, `Status "${status}"`)
}
aw.seed('shopConfig', { $id: 'f_sp1_shop', customerId: 'sc1', projectId: 'sp1', siteKey: SITE_KEY, anfrageStatus: 'angenommen' })
assert.deepEqual(await bestellpruefung({ customerId: 'sc1', projectId: 'sp1' }), [])
// ein fremder Kunde sieht die Anfrage nicht
assert.deepEqual(await bestellpruefung({ customerId: 'sc2', projectId: 'sp1' }), OFFEN)
})
test('Shop-Haken: Unterlagen sind bereit, sobald die Dateien da sind (bis Block J: nicht)', async () => {
const { unterlagenBereit } = reg.featureHaken('shop')
const { featureLegalReady, legalPaths } = await import('../server/services/legalDocs.js')
const { existsSync } = await import('node:fs')
const F = config.shop.fassung
const dateien = Object.values(legalPaths('shop', F)).flatMap((p) => [p, p.replace(/\.html$/, '.pdf')])
const alleDa = dateien.every((p) => existsSync(new URL(`../public${p}`, import.meta.url)))
assert.equal(await featureLegalReady('shop'), alleDa)
assert.equal(await unterlagenBereit(), alleDa)
})
test('Shop-Ticket: Details deterministisch, mit Snippet und Schritten', async () => {
aw.seed('shopConfig', {
$id: 'f_sp9_shop',
customerId: 'sc9',
projectId: 'sp9',
siteKey: SITE_KEY,
anfrage: JSON.stringify({ warenart: 'kleidung', beschreibung: 'Streng geheim', artikelzahl: 'bis100', versand: true, abholung: true, nachricht: 'Hallo' }),
anfrageStatus: 'angenommen',
})
const doc = { $id: 'f_sp9_shop', customerId: 'sc9', projectId: 'sp9', siteKey: SITE_KEY, featureKey: 'shop' }
const ctx = { feature: config.features.shop, doc, project: { projectName: 'Laden', liveDomain: 'laden.de' }, customer: { name: 'Frau Muster' } }
const text = await config.features.shop.ticket.details(ctx)
assert.equal(typeof text, 'string')
for (const teil of [
'Funktion: Webshop',
'Projekt: Laden',
'Domain: laden.de',
'Kunde: Frau Muster',
`siteKey: ${SITE_KEY}`,
'Anfrage: Warenart: Kleidung, Lieferung: Versand und Abholung, Artikelzahl: bis 100',
`<div data-webklar-shop="${SITE_KEY}"></div>`,
'<script src="https://project.webklar.com/embed/shop.js" defer></script>',
`https://project.webklar.com/shop/${SITE_KEY}/widerruf`,
`https://project.webklar.com/shop/${SITE_KEY}`,
'docs/funktionen/shop-datenschutz-absatz.md',
'Zeit im Arbeitsblatt',
'Verweis: ',
'/customers/sc9?tab=funktionen&anleitung=sp9',
]) {
assert.ok(text.includes(teil), `fehlt: ${teil}`)
}
// freier Text der Anfrage gehört nicht ins Ticket; deterministisch
assert.ok(!text.includes('Streng geheim'))
assert.equal(await config.features.shop.ticket.details(ctx), text)
assert.ok(text.length <= reg.TICKET_DETAILS_MAX)
// ohne Anfrage oder bei Lesefehler: dieselben Teile, nur ohne Anfrage-Zeile; wirft nie
const ohne = await config.features.shop.ticket.details({ ...ctx, doc: { ...doc, $id: 'f_unbekannt_shop' } })
assert.ok(!ohne.includes('Anfrage:'))
assert.ok(ohne.includes(`data-webklar-shop="${SITE_KEY}"`))
aw.seed('shopConfig', { $id: 'f_sp8_shop', customerId: 'sc9', projectId: 'sp8', siteKey: SITE_KEY, anfrage: '{kaputt' })
const kaputt = await config.features.shop.ticket.details({ ...ctx, doc: { ...doc, $id: 'f_sp8_shop' } })
assert.ok(!kaputt.includes('Anfrage:'))
const notfall = await config.features.shop.ticket.details(null)
assert.equal(typeof notfall, 'string')
assert.ok(notfall.length > 0)
})
test('Shop-Ticket über ticketTexte: Betreff und Details', async () => {
const doc = { $id: 'f_sp7_shop', customerId: 'sc7', projectId: 'sp7', siteKey: SITE_KEY, featureKey: 'shop' }
const { topic, details } = await reg.ticketTexte({ feature: config.features.shop, doc, project: { projectName: 'Laden' }, customer: null })
assert.equal(topic, 'Webshop einbauen – Laden')
assert.ok(details.includes(`data-webklar-shop="${SITE_KEY}"`))
})
+18 -3
View File
@@ -69,13 +69,14 @@ test('liste_zustaende', async () => {
assert.deepEqual(body.termine, { verfuegbar: false })
// Register-Ansicht (B12): Preise und Bestelltexte beider Funktionen aus config.features, auch bei termine.verfuegbar: false
assert.deepEqual(Object.keys(body.features).sort(), ['news', 'termine'])
assert.deepEqual(Object.keys(body.features).sort(), ['news', 'shop', 'termine'])
assert.equal(body.features.shop.priceLabel, '299 €')
assert.equal(body.features.news.priceLabel, '30 €')
assert.equal(body.features.termine.priceLabel, '149 €')
assert.equal(body.ustHinweis, config.ustHinweis)
assert.ok(typeof body.ustHinweis === 'string' && body.ustHinweis.length > 0)
// Items vollstaendig: Kennung, Wortlaut und Haken jedes Registerpunkts, dazu Fassung und Knopf
for (const key of ['news', 'termine']) {
for (const key of ['news', 'termine', 'shop']) {
const eintrag = config.features[key]
const ansicht = body.features[key]
assert.equal(ansicht.key, key)
@@ -111,7 +112,7 @@ 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', 'shop', 'termine'])
assert.equal(body.legal.news, null)
const termine = body.legal.termine
assert.deepEqual(Object.keys(termine).sort(), ['belehrung', 'links', 'version'])
@@ -130,6 +131,20 @@ test('liste_legal: legal.termine mit Fassung, https-Links mit Fassung und der Be
assert.deepEqual(body.features.termine.avv, { version: V, url: adressen.avv })
assert.deepEqual(body.features.termine.leistungsbeschreibung, { version: V, url: adressen.lb })
assert.deepEqual(body.features.news.widerruf, null)
// Webshop: eigene Fassung (config.shop.fassung), eigene Slugs, dieselbe Belehrung der Funktionen
const S = config.shop.fassung
const shop = body.legal.shop
assert.deepEqual(Object.keys(shop).sort(), ['belehrung', 'links', 'version'])
assert.equal(shop.version, S)
assert.deepEqual(Object.keys(shop.links), ['lb', 'avv', 'widerruf'])
const shopAdressen = legalPaths('shop', S)
for (const [doc, link] of Object.entries(shop.links)) assert.equal(link, `${config.publicBaseUrl}${shopAdressen[doc]}`)
assert.ok(shop.links.lb.includes('leistungsbeschreibung-webshop-'))
assert.ok(shop.links.avv.includes('avv-webshop-'))
assert.ok(shop.links.widerruf.includes('widerrufsbelehrung-funktionen-'))
assert.equal(shop.belehrung, widerrufsbelehrungFunktion())
assert.deepEqual(body.features.shop.avv, { version: S, url: shopAdressen.avv })
})
test('liste_register_ohne_freigabe: features.termine steht auch bei Schalter off und Pilot ohne Teilnahme', async () => {
+1 -1
View File
@@ -295,7 +295,7 @@ test('GET /api/funktionen: Projekte tragen features.termine und News wie bisher'
assert.equal(pt1.features.news.state, 'disabled')
const pt2 = body.projects.find((p) => p.projectId === 'pt2')
assert.deepEqual([pt2.features.termine.state, pt2.features.news.state], ['off', 'off'])
assert.deepEqual(Object.keys(pt2.features), ['news', 'termine'])
assert.deepEqual(Object.keys(pt2.features), ['news', 'termine', 'shop'])
// Abschaltgrund und Loeschfrist (E2, R105): News ist hier vom Team abgeschaltet: Grund und Zeitpunkt ja, ein Loeschdatum nein
// (der 30-Tage-Loeschlauf loescht nur Terminplaner-Daten); der aktive Terminplaner und das leere Projekt haben nichts
const aus = pt1.features.news
+54 -1
View File
@@ -1,6 +1,6 @@
import { test, after, mock } from 'node:test'
import assert from 'node:assert/strict'
import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync, writeSync } from 'node:fs'
import { closeSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync, writeSync } from 'node:fs'
import fsp from 'node:fs/promises'
import { syncBuiltinESMExports } from 'node:module'
import { tmpdir } from 'node:os'
@@ -291,6 +291,59 @@ test('News: Leistungsbeschreibung folgt der Fassung <F> (Adresse unverändert, k
assert.equal(Object.getOwnPropertyDescriptor(n, 'leistungsbeschreibung').get, undefined)
})
// Webshop (Block A3): eigener Satz mit eigener Fassung config.shop.fassung; die Dateien legt Block J an
test('Webshop-Satz: Adressen, PDF-Namen und Funktionszuordnung folgen config.shop.fassung', async () => {
const S = config.shop.fassung
assert.match(S, /^\d{4}-\d{2}-\d{2}$/)
assert.deepEqual(legalPaths('shop', S), {
lb: `/rechtliches/leistungsbeschreibung-webshop-${S}.html`,
avv: `/rechtliches/avv-webshop-${S}.html`,
widerruf: `/rechtliches/widerrufsbelehrung-funktionen-${S}.html`,
})
assert.deepEqual(Object.keys(legalPaths('shop', S)), ['lb', 'avv', 'widerruf'])
const f = legalPdfFiles('shop', S)
assert.equal(f.lb.filename, `Leistungsbeschreibung-Webshop-${S}.pdf`)
assert.equal(f.avv.filename, `AVV-Webshop-${S}.pdf`)
assert.equal(f.widerruf.filename, `Widerrufsbelehrung-Muster-Widerrufsformular-${S}.pdf`)
assert.equal(f.avv.path, new URL(`../public/rechtliches/avv-webshop-${S}.pdf`, import.meta.url).pathname)
assert.throws(() => legalPaths('shop', '2026-10-5'), /Fassung/)
// die aktuelle Fassung des Satzes shop ist config.shop.fassung (nicht die der Termine)
assert.deepEqual(featureLegalPaths('shop'), legalPaths('shop', S))
const alt = config.shop.fassung
config.shop.fassung = '2099-01-02'
try {
assert.deepEqual(featureLegalPaths('shop'), legalPaths('shop', '2099-01-02'))
assert.deepEqual(featureLegalPaths('termine'), legalPaths('termine', V), 'Termine bleiben bei ihrer Fassung')
await assert.rejects(featureLegalAttachments('shop'))
} finally {
config.shop.fassung = alt
}
assert.deepEqual(featureLegalPaths('shop', { fassung: '2026-10-01' }), legalPaths('shop', '2026-10-01'))
assert.deepEqual(featureLegalPaths('termine'), legalPaths('termine', V))
})
test('featureLegalReady("shop") ist true genau dann, wenn alle sechs Dateien der Fassung da sind (bis Block J: false)', async () => {
const S = config.shop.fassung
const html = Object.values(legalPaths('shop', S))
const pdfs = Object.values(legalPdfFiles('shop', S)).map((x) => x.path)
const alleDa = html.every((p) => existsSync(root(`public${p}`))) && pdfs.every((p) => existsSync(p))
assert.equal(await featureLegalReady('shop'), alleDa)
assert.equal(await featureLegalReady('shop', { fassung: '1999-01-01' }), false)
// nie ein Wurf bei fehlenden Dateien; die Anhaenge werfen dagegen
if (!alleDa) await assert.rejects(featureLegalAttachments('shop'))
})
test('Register: leistungsbeschreibung, avv und widerruf des Webshops folgen config.shop.fassung (Drift-Wächter)', () => {
const s = featureConfig('shop')
const S = config.shop.fassung
const p = legalPaths('shop', S)
assert.deepEqual(s.leistungsbeschreibung, { version: S, url: p.lb })
assert.deepEqual(s.avv, { version: S, url: p.avv })
assert.deepEqual(s.widerruf, { version: S, url: p.widerruf })
assert.equal(s.consent.version, S)
})
test('config.js importiert legalDocs.js nicht (Adresse aus Slug und Version gebaut)', () => {
const src = readFileSync(root('server/config.js'), 'utf8')
assert.doesNotMatch(src, /^\s*(import\b[^\n]*|.*\bimport\()['"`][^'"`]*legalDocs/m)
+4 -3
View File
@@ -185,17 +185,18 @@ test('termine_ist_zweite_funktion: Schluessel und Bezeichnungen', () => {
aw.seed('websiteProjects', { $id: 'pt1', customerId: 'c1', projectName: 'Salon' })
aw.seed('websiteProjects', { $id: 'pt2', customerId: 'c1', projectName: 'Studio' })
aw.seed('websiteProjects', { $id: 'pt3', customerId: 'c2', projectName: 'Fremder Salon' })
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine'])
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'shop'])
assert.deepEqual(sf.FEATURE_LABELS, {
news: { name: 'News-Pinnwand', keine: 'keine News-Pinnwand' },
termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' },
shop: { name: 'Webshop', keine: 'keinen Webshop' },
})
})
test('liste_zeigt_termine_je_projekt_ohne_dokument_als_off', async () => {
const list = await sf.listCustomerFeatures('c1')
const p = list.find((x) => x.projectId === 'pt1')
assert.deepEqual(Object.keys(p.features), ['news', 'termine'])
assert.deepEqual(Object.keys(p.features), ['news', 'termine', 'shop'])
assert.equal(p.features.termine.state, 'off')
assert.equal(p.features.termine.siteKey, null)
assert.equal(p.features.termine.ticketWoid, null)
@@ -261,7 +262,7 @@ test('activateClosedTickets: Funktionen mit ticketFreischaltung; News verhaelt s
const ids = ['f_setup_termine', 'f_setup_news', 'f_setup_analyse']
const reg = await import('../server/services/featureRegistry.js')
try {
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine'])
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'shop'])
// News wie bisher; Termine wartet auf Einstellung und Abo (Haken freischaltbar); Analyse hat keine ticketFreischaltung
const done = await sf.activateClosedTickets({ customerId: 'c1' })
assert.deepEqual(done.map((d) => d.featureKey), ['news'])