Files

504 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// 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()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const reg = await import('../server/services/featureRegistry.js')
const sf = await import('../server/services/siteFeatures.js')
const { createFeatureTicket } = await import('../server/services/ticketAdmin.js')
// Wortlaut-Pin des News-Satzes und der Fassung (der einzige Literal-Pin auf Texte, die Block F2 ändert); die Fassung ist der
// Tag <F> = config.termine.avvVersion, beim Anheben von <F> ändert sich auch dieser Pin
const NEWS_SATZ =
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung des Einbaus 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).'
const NEWS_FASSUNG = '2026-10-05'
const SITE_KEY = 'abcdefghijklmnop'
// ---------------------------------------------------------------------------------- News (Festwerte) ---
test('News-Eintrag entspricht den bisherigen Festwerten', async () => {
const news = reg.featureConfig('news')
assert.equal(news.key, 'news')
assert.equal(news.product, 'newsSetup')
assert.deepEqual(news.label, { name: 'News-Pinnwand', kurz: 'News', keine: 'keine News-Pinnwand' })
assert.equal(news.priceLabel, '30 €')
assert.equal(news.setupEstimate, 'meist innerhalb von 1–2 Wochen')
assert.equal(news.consent.version, NEWS_FASSUNG)
assert.equal(news.consent.knopf, 'Zahlungspflichtig bestellen · 30 €')
assert.deepEqual(news.consent.items, [{ id: 'beginn', haken: true, text: NEWS_SATZ }])
assert.deepEqual(news.leistungsbeschreibung, { version: NEWS_FASSUNG, url: '/vertragsunterlagen/funktionen-leistungsbeschreibung.html' })
assert.equal(news.avv, null)
assert.equal(news.widerruf, null)
assert.equal(news.returnHash, 'news')
assert.equal(news.returnProjekt, false)
assert.equal(news.requiresWartungsAbo, false)
assert.equal(news.ticketFreischaltung, true)
assert.equal(
news.disabledMessage,
'Die News-Pinnwand wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du sie wieder möchtest.'
)
assert.equal(news.disabledMessage, sf.DISABLED_MESSAGE)
assert.equal(news.orderedConflictMessage, 'Die News-Pinnwand ist für dieses Projekt schon bestellt.')
assert.equal(news.unavailableMessage, undefined)
assert.equal(news.ticket.topic({ projectName: 'Bäckerei' }), 'News-Pinnwand einbauen – Bäckerei')
assert.equal(news.ticket.topic({ subdomain: 'cafe' }), 'News-Pinnwand einbauen – cafe')
assert.equal(news.ticket.topic(null), 'News-Pinnwand einbauen –')
assert.deepEqual(news.mails, {
funktion: 'News-Pinnwand',
formen: { dein: 'Deine News-Pinnwand', nom: 'die News-Pinnwand', akk: 'die News-Pinnwand', gen: 'der News-Pinnwand', pron: 'sie' },
knopf: 'Beitrag schreiben',
danach: 'Danach kannst Du Beiträge im Portal schreiben.',
bereit: 'Du kannst ab sofort Beiträge schreiben, sie erscheinen direkt bei Dir auf der Seite.',
preisHinweis: news.mails.preisHinweis,
anleitung: false,
})
assert.match(news.mails.preisHinweis, /einmalig/)
})
test('News-Ticketdetails sind die sechs Zeilen des bisherigen Standards', async () => {
const news = reg.featureConfig('news')
const project = { $id: 'p1', projectName: 'Bäckerei', liveDomain: 'baeckerei.de', previewUrl: 'https://p.example', subdomain: 'baeckerei' }
const ctx = { feature: news, doc: { siteKey: SITE_KEY, featureKey: 'news' }, project, customer: { name: 'Kunde' } }
const expected = [
'Funktion: news',
'Projekt: Bäckerei',
'Domain: baeckerei.de',
`siteKey: ${SITE_KEY}`,
`Einbau: <div data-webklar-news="${SITE_KEY}"></div> + https://project.webklar.com/embed/news.js`,
'Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)',
].join('\n')
assert.equal(await news.ticket.details(ctx), expected)
// Domain: liveDomain || previewUrl || subdomain; Projektname: projectName || subdomain
const ohneLive = await news.ticket.details({ ...ctx, project: { projectName: 'X', previewUrl: 'https://p.example', subdomain: 's' } })
assert.match(ohneLive, /^Domain: https:\/\/p\.example$/m)
const nurSub = await news.ticket.details({ ...ctx, project: { subdomain: 's' } })
assert.match(nurSub, /^Projekt: s$/m)
assert.match(nurSub, /^Domain: s$/m)
// Parität zu createFeatureTicket (Standardtexte): dasselbe Dokument, Wort für Wort
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin' })
const { ticketId } = await createFeatureTicket({ customerId: 'c1', project, featureKey: 'news', siteKey: SITE_KEY })
const ticket = aw.docs('workorders').get(ticketId)
assert.equal(ticket.details, expected)
assert.equal(ticket.topic, news.ticket.topic(project))
})
// ---------------------------------------------------------------------------------- Alias funktionen ---
test('Alias bleibt: config.funktionen folgt config.features.news zur Laufzeit', () => {
const news = config.features.news
assert.equal(config.funktionen.setupEstimate, news.setupEstimate)
assert.equal(config.funktionen.consentText, news.consent.items[0].text)
assert.equal(config.funktionen.consentText, NEWS_SATZ)
const vorherEstimate = news.setupEstimate
const vorherText = news.consent.items[0].text
try {
news.setupEstimate = 'in 3 Tagen'
news.consent.items[0].text = 'Neuer Satz'
assert.equal(config.funktionen.setupEstimate, 'in 3 Tagen')
assert.equal(config.funktionen.consentText, 'Neuer Satz')
} finally {
news.setupEstimate = vorherEstimate
news.consent.items[0].text = vorherText
}
assert.equal(config.funktionen.setupEstimate, vorherEstimate)
assert.equal(config.funktionen.consentText, vorherText)
})
// ---------------------------------------------------------------------------------- siteFeatures ---
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' },
unterkunft: { name: 'Buchungskalender', keine: 'keinen Buchungskalender' },
zeiten: { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' },
})
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'shop', 'unterkunft', 'zeiten'])
assert.deepEqual(sf.FEATURE_KEYS, reg.featureKeys())
assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'shop', 'unterkunft', 'zeiten'])
})
// ---------------------------------------------------------------------------------- Schlüsselschutz ---
test('Schlüsselschutz: nur eigene Schlüssel des Registers', () => {
for (const key of ['analyse', '__proto__', 'constructor', 'toString', 'hasOwnProperty', undefined, null, '', 'News']) {
assert.equal(reg.featureConfig(key), null, `featureConfig(${String(key)})`)
assert.equal(reg.oeffentlicheAnsicht(key), null, `oeffentlicheAnsicht(${String(key)})`)
assert.deepEqual(reg.featureHaken(key), {}, `featureHaken(${String(key)})`)
}
assert.equal(reg.featureConfig('news'), config.features.news)
assert.equal(reg.featureConfig('termine'), config.features.termine)
})
// ---------------------------------------------------------------------------------- Haken ---
test('Haken überschreibbar: config.features.<key>.<haken> gewinnt gegen den Standard', () => {
const termine = config.features.termine
const vorherTermine = reg.featureHaken('termine')
const vorherNews = reg.featureHaken('news')
const fn = () => []
try {
termine.bestellpruefung = fn
const haken = reg.featureHaken('termine')
assert.equal(haken.bestellpruefung, fn)
// die anderen Haken bleiben beim Standard
for (const name of ['verfuegbar', 'unterlagenBereit', 'belehrung', 'freischaltbar']) assert.equal(haken[name], vorherTermine[name])
} finally {
delete termine.bestellpruefung
}
assert.deepEqual(reg.featureHaken('termine'), vorherTermine)
// jeder der sechs Haken lässt sich setzen; Nicht-Funktionen und fremde Namen zählen nicht
const namen = ['verfuegbar', 'bestellpruefung', 'unterlagenBereit', 'belehrung', 'freischaltbar', 'nachFreischaltung']
const news = config.features.news
try {
for (const name of namen) news[name] = () => name
news.unbekannt = () => 'x'
const haken = reg.featureHaken('news')
assert.deepEqual(Object.keys(haken).sort(), [...namen].sort())
for (const name of namen) assert.equal(haken[name](), name)
news.verfuegbar = 'kein Haken'
assert.equal(reg.featureHaken('news').verfuegbar, vorherNews.verfuegbar)
} finally {
for (const name of [...namen, 'unbekannt']) delete news[name]
}
assert.deepEqual(reg.featureHaken('news'), vorherNews)
})
// ---------------------------------------------------------------------------------- öffentliche Ansicht ---
test('Ansicht ohne Funktionen: JSON-sicher, ohne ticket und mails', () => {
for (const key of reg.featureKeys()) {
const ansicht = reg.oeffentlicheAnsicht(key)
assert.deepEqual(JSON.parse(JSON.stringify(ansicht)), ansicht)
assert.deepEqual(Object.keys(ansicht).sort(), [
'avv',
'consent',
'key',
'kurz',
'leistungsbeschreibung',
'name',
'priceLabel',
'requiresWartungsAbo',
'returnHash',
'setupEstimate',
'widerruf',
])
assert.equal(ansicht.ticket, undefined)
assert.equal(ansicht.mails, undefined)
assert.deepEqual(Object.keys(ansicht.consent).sort(), ['items', 'knopf', 'version'])
for (const item of ansicht.consent.items) assert.deepEqual(Object.keys(item).sort(), ['haken', 'id', 'text'])
}
const news = reg.oeffentlicheAnsicht('news')
assert.equal(news.name, 'News-Pinnwand')
assert.equal(news.kurz, 'News')
assert.equal(news.priceLabel, '30 €')
assert.equal(news.consent.knopf, 'Zahlungspflichtig bestellen · 30 €')
assert.deepEqual(news.consent.items, [{ id: 'beginn', text: NEWS_SATZ, haken: true }])
assert.equal(news.returnHash, 'news')
assert.equal(news.requiresWartungsAbo, false)
assert.equal(news.avv, null)
// eine Kopie: Änderungen an der Ansicht ändern das Register nicht
news.consent.items[0].text = 'verändert'
news.leistungsbeschreibung.version = 'verändert'
assert.equal(config.features.news.consent.items[0].text, NEWS_SATZ)
assert.equal(config.features.news.leistungsbeschreibung.version, NEWS_FASSUNG)
})
// ---------------------------------------------------------------------------------- Termine ---
test('Termine: Preis und Struktur', async () => {
const t = reg.featureConfig('termine')
assert.equal(t.key, 'termine')
assert.equal(t.product, 'termineSetup')
assert.deepEqual(t.label, { name: 'Terminplaner', kurz: 'Termine', keine: 'keinen Terminplaner' })
assert.equal(t.priceLabel, '149 €')
assert.equal(t.setupEstimate, 'meist innerhalb von 1–2 Wochen')
assert.equal(t.consent.knopf, 'Zahlungspflichtig bestellen · 149 €')
assert.deepEqual(
t.consent.items.map((i) => i.id),
['unterlagen', 'gesundheit', 'beginn', 'information']
)
assert.deepEqual(
t.consent.items.map((i) => i.haken),
[true, true, true, true]
)
for (const item of t.consent.items) {
assert.equal(typeof item.text, 'string')
assert.ok(item.text.length > 0)
assert.equal(item.url, undefined, 'ohne url')
}
// Task F7: die Widerrufsbelehrung der Funktionen trägt die Fassung des Registers und die versionierte Adresse
assert.deepEqual(t.widerruf, { version: config.termine.avvVersion, url: `/rechtliches/widerrufsbelehrung-funktionen-${config.termine.avvVersion}.html` })
assert.equal(t.consent.version, config.termine.avvVersion)
assert.equal(t.returnHash, 'termine')
assert.equal(t.returnProjekt, true)
assert.equal(t.requiresWartungsAbo, true)
assert.equal(t.ticketFreischaltung, true)
assert.equal(t.disabledMessage, 'Der Terminplaner ist für diese Website abgeschaltet. Schreib uns im Chat, wenn Du ihn wieder möchtest.')
assert.equal(t.orderedConflictMessage, 'Der Terminplaner ist für dieses Projekt schon bestellt.')
assert.equal(t.unavailableMessage, 'Der Terminplaner ist noch nicht verfügbar.')
assert.equal(config.ustHinweis, 'ohne Umsatzsteuer gemäß § 19 UStG')
assert.equal(t.mails.preisHinweis, `einmalig, ${config.ustHinweis}; der Terminplaner läuft, solange Dein Wartungs-Abo läuft`)
assert.ok(t.mails.preisHinweis.includes(config.ustHinweis))
assert.equal(t.mails.funktion, 'Terminplaner')
assert.deepEqual(t.mails.formen, {
dein: 'Dein Terminplaner',
nom: 'der Terminplaner',
akk: 'den Terminplaner',
gen: 'des Terminplaners',
pron: 'ihn',
})
assert.equal(t.mails.knopf, 'Termine öffnen')
assert.equal(t.mails.anleitung, true)
assert.equal(typeof t.mails.danach, 'string')
assert.equal(typeof t.mails.bereit, 'string')
assert.equal(t.ticket.topic({ projectName: 'Salon' }), 'Terminplaner einbauen – Salon')
assert.equal(t.ticket.topic({ subdomain: 'salon' }), 'Terminplaner einbauen – salon')
assert.equal(typeof t.ticket.details, 'function')
const ansicht = reg.oeffentlicheAnsicht('termine')
assert.equal(ansicht.name, 'Terminplaner')
assert.equal(ansicht.kurz, 'Termine')
assert.equal(ansicht.priceLabel, '149 €')
assert.equal(ansicht.requiresWartungsAbo, true)
assert.equal(ansicht.returnHash, 'termine')
})
test('Termine-Ticketdetails werden erst beim Aufruf geladen', async () => {
// das Ziel termine/ticketDetails.js entsteht in Task B8; config.js importiert keine Dienste statisch
const { readFileSync } = await import('node:fs')
const src = readFileSync(new URL('../server/config.js', import.meta.url), 'utf8')
assert.doesNotMatch(src, /^import .* from '\.\/services\//m)
assert.match(src, /import\('\.\/services\/termine\/ticketDetails\.js'\)/)
})
// ---------------------------------------------------------------------------------- Block F ---
test('Bestelltexte ohne ENTWURF', async () => {
const t = config.features.termine
const frei = (wert, was) => assert.doesNotMatch(JSON.stringify(wert), /ENTWURF/, `${was} enthält ENTWURF`)
frei(t.consent.version, 'consent.version')
frei(t.consent.items, 'consent.items')
frei(t.leistungsbeschreibung, 'leistungsbeschreibung')
frei(t.avv, 'avv')
frei(t.widerruf, 'widerruf')
// mails.danach und mails.bereit schreibt der Inhaber in seiner Mailrunde
// der Haken belehrung liefert seit F7 die Widerrufsbelehrung der Funktionen
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}"`))
})