Die Öffnungszeiten stehen jetzt hinter dem Webshop (news, termine, shop, zeiten): Register, FEATURE_KEYS und ticketFreischaltungKeys in zeitenRegister, die drei Listen in app.js (VIEW_HASHES, APP_PARENT, FEATURE_LABELS) in zeitenBestellen und shopWiring (APP_PARENT endet mit webshop, zeiten). funktionenZeitenTile prüft die Importe von funktionen.js gegen den Stand von main (fn-util.js und analyse-view.js); die Kachel der Öffnungszeiten bringt weiterhin keinen neuen Import mit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
11 KiB
JavaScript
213 lines
11 KiB
JavaScript
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-Eintrag config.features.zeiten ("Öffnungszeiten live"), freiweg() und der Stripe-Katalog zeitenSetup.
|
||
// Dienste (siteFeatures, stripeBilling) 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 { ensurePrice } = await import('../server/services/stripeBilling.js')
|
||
|
||
const feature = () => reg.featureConfig('zeiten')
|
||
|
||
test('zeiten steht im Register mit 30 € und Abo-Inklusive', () => {
|
||
const f = feature()
|
||
assert.equal(f, config.features.zeiten)
|
||
assert.equal(f.key, 'zeiten')
|
||
assert.equal(f.product, 'zeitenSetup')
|
||
assert.deepEqual(f.label, { name: 'Öffnungszeiten live', kurz: 'Zeiten', keine: 'keine Öffnungszeiten live' })
|
||
assert.equal(f.priceLabel, '30 €')
|
||
assert.equal(f.setupEstimate, 'meist innerhalb weniger Tage')
|
||
assert.equal(f.requiresWartungsAbo, false)
|
||
assert.equal(f.aboInklusive, true)
|
||
assert.equal(f.ticketFreischaltung, true)
|
||
assert.deepEqual(f.teilweiseLive, ['setup', 'active'])
|
||
assert.equal(f.returnHash, 'zeiten')
|
||
assert.equal(f.returnProjekt, true)
|
||
assert.deepEqual(f.leistungsbeschreibung, { version: '2026-10-04', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' })
|
||
assert.equal(f.avv, null)
|
||
assert.equal(f.widerruf, null)
|
||
assert.equal(f.disabledMessage, 'Öffnungszeiten live wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du die Funktion wieder möchtest.')
|
||
assert.equal(f.orderedConflictMessage, 'Öffnungszeiten live ist für dieses Projekt schon bestellt oder eingeschaltet.')
|
||
assert.equal(f.unavailableMessage, 'Öffnungszeiten live ist noch nicht verfügbar.')
|
||
// Funktionsschlüssel und Katalog stehen in den abgeleiteten Listen
|
||
assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'shop', 'zeiten'])
|
||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'shop', 'zeiten'])
|
||
assert.deepEqual(sf.FEATURE_LABELS.zeiten, { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' })
|
||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'shop', 'zeiten'])
|
||
})
|
||
|
||
test('Zustimmung bezahlt: zwei Haken, Knopf „Zahlungspflichtig bestellen · 30 €“, § 356 Abs. 5 Nr. 2', () => {
|
||
const { consent } = feature()
|
||
assert.equal(consent.version, '2026-10-05', 'Fassung nach dem Wertersatz-Satz (§ 357a Abs. 2 BGB)')
|
||
assert.equal(consent.knopf, 'Zahlungspflichtig bestellen · 30 €')
|
||
assert.deepEqual(consent.items.map((i) => [i.id, i.haken]), [['leistung', true], ['beginn', true]])
|
||
assert.equal(
|
||
consent.items[0].text,
|
||
'Ich habe die Leistungsbeschreibung „Öffnungszeiten live“ gelesen und bestelle die Einrichtung.'
|
||
)
|
||
assert.equal(
|
||
consent.items[1].text,
|
||
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit dem Einbau beginnt. Mir ist bekannt, dass mein Widerrufsrecht mit vollständiger Erbringung der Leistung 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).'
|
||
)
|
||
assert.match(consent.items[1].text, /§ 356 Abs\. 5 Nr\. 2/)
|
||
})
|
||
|
||
test('Zustimmung bezahlt: der Wertersatz-Satz ist wörtlich der von News und Terminplaner (einheitliche Fassungen; Wortlaut bleibt Sache der Rechtsrunde, ZQ3)', () => {
|
||
const satz = (text) => text.match(/Widerrufe ich vor der vollständigen Erbringung[^.]*\.[^.]*\)\./)?.[0]
|
||
const zeiten = satz(config.features.zeiten.consent.items.find((i) => i.id === 'beginn').text)
|
||
assert.ok(zeiten, 'Wertersatz-Satz vorhanden')
|
||
for (const key of ['news', 'termine']) {
|
||
assert.equal(satz(config.features[key].consent.items.find((i) => i.id === 'beginn').text), zeiten, `${key} hat denselben Satz`)
|
||
}
|
||
})
|
||
|
||
test('Zustimmung gratis: ein Haken, Knopf „Einschalten“, kein „zahlungspflichtig“', () => {
|
||
const f = feature()
|
||
assert.equal(f.consentFrei.version, '2026-10-04')
|
||
assert.equal(f.consentFrei.knopf, 'Einschalten')
|
||
assert.deepEqual(f.consentFrei.items, [
|
||
{
|
||
id: 'leistung',
|
||
haken: true,
|
||
text: 'Ich habe die Leistungsbeschreibung „Öffnungszeiten live“ gelesen und schalte die Funktion ein. Sie ist in meinem Wartungs-Abo ohne Aufpreis enthalten.',
|
||
},
|
||
])
|
||
assert.doesNotMatch(JSON.stringify(f.consentFrei), /zahlungspflichtig/i)
|
||
})
|
||
|
||
test('freiweg ersetzt nur consent', () => {
|
||
const f = feature()
|
||
const frei = reg.freiweg(f)
|
||
assert.equal(frei.consent, f.consentFrei)
|
||
assert.notEqual(frei, f, 'eine Kopie, das Register bleibt unberührt')
|
||
assert.equal(f.consent.knopf, 'Zahlungspflichtig bestellen · 30 €')
|
||
assert.equal(frei.product, f.product)
|
||
assert.equal(frei.key, f.key)
|
||
assert.equal(frei.mails, f.mails)
|
||
assert.deepEqual(Object.keys(frei).sort(), Object.keys(f).sort())
|
||
for (const key of Object.keys(f)) if (key !== 'consent') assert.equal(frei[key], f[key], key)
|
||
})
|
||
|
||
test('verfuegbar folgt ZEITEN_ENABLED', () => {
|
||
const f = feature()
|
||
const vorher = { ...config.zeiten }
|
||
try {
|
||
config.zeiten.mode = 'off'
|
||
assert.equal(reg.featureSperre('zeiten', 'a@b.de'), f.unavailableMessage)
|
||
config.zeiten.mode = 'on'
|
||
assert.equal(reg.featureSperre('zeiten', 'a@b.de'), null)
|
||
config.zeiten.mode = 'pilot'
|
||
config.zeiten.pilotEmails = ['pilot@b.de']
|
||
assert.equal(reg.featureSperre('zeiten', 'a@b.de'), f.unavailableMessage)
|
||
assert.equal(reg.featureSperre('zeiten', 'pilot@b.de'), null)
|
||
} finally {
|
||
Object.assign(config.zeiten, vorher)
|
||
}
|
||
})
|
||
|
||
test('Standard-Haken: verfuegbar, unterlagenBereit, belehrung, freischaltbar, nachFreischaltung (keine Bestellprüfung)', async () => {
|
||
const haken = reg.featureHaken('zeiten')
|
||
assert.deepEqual(Object.keys(haken).sort(), ['belehrung', 'freischaltbar', 'nachFreischaltung', 'unterlagenBereit', 'verfuegbar'])
|
||
const termine = reg.featureHaken('termine')
|
||
assert.equal(await haken.unterlagenBereit(), true)
|
||
assert.equal(await haken.belehrung(), await termine.belehrung(), 'wie beim Terminplaner')
|
||
// kein Platzhalter mehr (der alte Haken lieferte immer true): Verhalten und Nacharbeit prüft test/zeitenFreischalten.test.js
|
||
for (const name of ['freischaltbar', 'nachFreischaltung']) assert.equal(typeof haken[name], 'function', name)
|
||
})
|
||
|
||
test('oeffentlicheAnsicht(zeiten) hat dieselben Schlüssel wie news', () => {
|
||
const zeiten = reg.oeffentlicheAnsicht('zeiten')
|
||
const news = reg.oeffentlicheAnsicht('news')
|
||
assert.deepEqual(Object.keys(zeiten).sort(), Object.keys(news).sort())
|
||
assert.deepEqual(Object.keys(zeiten.consent).sort(), Object.keys(news.consent).sort())
|
||
assert.equal(zeiten.aboInklusive, undefined)
|
||
assert.equal(zeiten.consentFrei, undefined)
|
||
assert.equal(zeiten.teilweiseLive, undefined)
|
||
assert.doesNotMatch(JSON.stringify(zeiten), /aboInklusive|consentFrei|Einschalten/)
|
||
assert.deepEqual(JSON.parse(JSON.stringify(zeiten)), zeiten)
|
||
assert.equal(zeiten.requiresWartungsAbo, false)
|
||
assert.equal(zeiten.name, 'Öffnungszeiten live')
|
||
assert.equal(zeiten.priceLabel, '30 €')
|
||
})
|
||
|
||
test('Mails: Werte aus dem Register, Preis aus zeitenSetup', () => {
|
||
const f = feature()
|
||
assert.deepEqual(f.mails, {
|
||
funktion: 'Öffnungszeiten live',
|
||
formen: { dein: 'Deine Öffnungszeiten live', nom: 'Öffnungszeiten live', akk: 'Öffnungszeiten live', gen: 'von Öffnungszeiten live', pron: 'sie' },
|
||
knopf: 'Zeiten bearbeiten',
|
||
danach: 'Danach kannst Du Deine Zeiten im Portal eintragen.',
|
||
bereit: 'Deine Öffnungszeiten stehen ab sofort live auf Deiner Seite.',
|
||
preisHinweis: 'einmalig, ohne Umsatzsteuer nach § 19 UStG; die Nutzung ist kostenlos, es läuft kein Abo',
|
||
anleitung: false,
|
||
})
|
||
const k15 = sf.bestellMail(f, { kundeName: 'Kunde Eins', projekt: 'Salon', invoiceUrl: '' })
|
||
assert.equal(k15.values.preis, '30,00 €')
|
||
assert.equal(k15.values.portal_url, `${config.publicBaseUrl}/dashboard.html#zeiten`)
|
||
})
|
||
|
||
test('Ticket: Betreff mit Projektname, Details beginnen mit den Notfall-Details (Einbau-Text: test/zeitenTicket.test.js)', async () => {
|
||
const f = feature()
|
||
const project = { projectName: 'Salon Haarlinie', subdomain: 'salon' }
|
||
assert.equal(f.ticket.topic(project), 'Öffnungszeiten live einbauen – Salon Haarlinie')
|
||
assert.equal(f.ticket.topic({ subdomain: 'salon' }), 'Öffnungszeiten live einbauen – salon')
|
||
const ctx = { feature: f, doc: { customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop' }, project, customer: { name: 'Kunde Eins' } }
|
||
const details = await f.ticket.details(ctx)
|
||
assert.ok(details.startsWith(reg.minimaleTicketDetails(ctx)))
|
||
assert.match(details, /^Funktion: zeiten$/m)
|
||
assert.match(details, /data-wk-zeiten src="https:\/\/project\.webklar\.com\/embed\/zeiten\.js" data-site="abcdefghijklmnop"/)
|
||
const texte = await reg.ticketTexte(ctx)
|
||
assert.equal(texte.topic, 'Öffnungszeiten live einbauen – Salon Haarlinie')
|
||
assert.equal(texte.details, details)
|
||
})
|
||
|
||
test('Stripe-Katalog: zeitenSetup mit eigener Metadaten-Marke', async () => {
|
||
assert.deepEqual(config.zeitenSetup, {
|
||
key: 'zeitenSetup',
|
||
name: 'Öffnungszeiten live – Einrichtung',
|
||
amountCents: 3000,
|
||
lookupKey: 'webklar_zeiten_einrichtung_3000',
|
||
oneTime: true,
|
||
priceNickname: 'Öffnungszeiten-Einrichtung 30 €',
|
||
})
|
||
const before = st.calls.length
|
||
const id = await ensurePrice('zeitenSetup')
|
||
const calls = st.calls.slice(before)
|
||
const preise = calls.filter((c) => c.method === 'POST' && c.path === '/v1/prices')
|
||
assert.equal(preise.length, 1)
|
||
assert.equal(preise[0].body['unit_amount'], '3000')
|
||
assert.equal(preise[0].body['lookup_key'], 'webklar_zeiten_einrichtung_3000')
|
||
assert.equal(preise[0].body['nickname'], 'Öffnungszeiten-Einrichtung 30 €')
|
||
assert.ok(!Object.keys(preise[0].body).some((k) => k.startsWith('recurring')), 'kein recurring')
|
||
assert.equal(st.prices.get(id).recurring, null)
|
||
const produkte = calls.filter((c) => c.method === 'POST' && c.path === '/v1/products')
|
||
assert.equal(produkte.length, 1)
|
||
assert.equal(produkte[0].body['name'], 'Öffnungszeiten live – Einrichtung')
|
||
assert.equal(produkte[0].body['metadata[webklar]'], 'zeiten-einrichtung')
|
||
// zweiter Aufruf: kein zweiter Preis
|
||
const again = st.calls.length
|
||
assert.equal(await ensurePrice('zeitenSetup'), id)
|
||
assert.equal(st.calls.slice(again).filter((c) => c.method === 'POST').length, 0)
|
||
})
|
||
|
||
test('Skript stripe-products kennt zeitenSetup', async () => {
|
||
const realExit = process.exit
|
||
process.exit = () => {}
|
||
let script
|
||
try {
|
||
script = await import('../scripts/stripe-products.mjs')
|
||
} finally {
|
||
process.exit = realExit
|
||
}
|
||
assert.ok(script.KEYS.includes('zeitenSetup'))
|
||
assert.equal(script.catalogEntry('zeitenSetup'), config.zeitenSetup)
|
||
})
|