Files
Webklar-Kundenbereich/test/zeitenRegister.test.js
T
ClaudeandClaude Opus 5.5 11dc4b2c9f Öffnungszeiten: Freischalten erst mit eingetragenen Zeiten
Der 5-Minuten-Job setzt Öffnungszeiten live nach dem Schließen des Tickets
nur noch auf active, wenn der Kunde Zeiten eingetragen hat (Haken
freischaltbar: Hauptplan gespeichert, mindestens ein Tag nicht "zu";
Lesefehler ergeben false, das Dokument bleibt in setup). Neuer Register-Haken
nachFreischaltung: runActivation ruft ihn je freigeschaltetem Dokument vor der
K16 auf, Fehler werden nur geloggt; für die Zeiten merkt er den Ersatz-Hash
(wochenplanHash). Selbstheilung (Spec 3.2): im Lauf des Jobs (nicht in der
Projektliste mit customerId) holt holeZeitenNach für Zeiten in setup fehlendes
Ticket und K24 nach, je Dokument abgesichert und ohne Kundendaten im Log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 02:28:52 +00:00

205 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, 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')
assert.equal(await haken.belehrung(), reg.BELEHRUNG_PLATZHALTER)
// 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)
})