Files
Webklar-Kundenbereich/test/zeitenRegister.test.js
T

204 lines
10 KiB
JavaScript
Raw 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-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', 'zeiten'])
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'zeiten'])
assert.deepEqual(sf.FEATURE_LABELS.zeiten, { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' })
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'zeiten'])
})
test('Zustimmung bezahlt: zwei Haken, Knopf „Zahlungspflichtig bestellen · 30 €“, § 356 Abs. 5 Nr. 2', () => {
const { consent } = feature()
assert.equal(consent.version, '2026-10-04')
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).'
)
assert.match(consent.items[1].text, /§ 356 Abs\. 5 Nr\. 2/)
})
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 (keine Bestellprüfung)', async () => {
const haken = reg.featureHaken('zeiten')
assert.deepEqual(Object.keys(haken).sort(), ['belehrung', 'freischaltbar', 'unterlagenBereit', 'verfuegbar'])
const termine = reg.featureHaken('termine')
assert.equal(await haken.unterlagenBereit(), true)
assert.equal(await haken.belehrung(), await termine.belehrung(), 'wie beim Terminplaner')
assert.equal(await haken.belehrung(), reg.BELEHRUNG_PLATZHALTER)
assert.equal(await haken.freischaltbar({ $id: 'x' }), true, 'Platzhalter bis Task 18')
})
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)
})