Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.
Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.
Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
e87140e5ba
245 files changed
+38799
-1050
No files matched your search
@@ -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,11 +121,12 @@ 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' },
|
||||
zeiten: { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' },
|
||||
})
|
||||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'zeiten'])
|
||||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'shop', 'zeiten'])
|
||||
assert.deepEqual(sf.FEATURE_KEYS, reg.featureKeys())
|
||||
assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'zeiten'])
|
||||
assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'shop', 'zeiten'])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------- Schlüsselschutz ---
|
||||
@@ -296,3 +297,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}"`))
|
||||
})
|
||||
Reference in new issue
Block a user