Öffnungszeiten: Schalter, Collections und Felder

ZEITEN_ENABLED/ZEITEN_PILOT_EMAILS/ZEITEN_KEIN_NEUBAU als config.zeiten (neben config.termine),
zeitenSetup (30 € einmalig), Collection-IDs oeffnungszeiten und zeitenErinnerungen, Schema der beiden
Collections plus fünf neue portalFeatures-Felder, scripts/zeiten-setup.mjs (idempotent, --check),
schemaKeys kennt die neuen Collections und Felder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 23:25:37 +00:00
1 parent 385f768d76
commit 4b4cd1058f
7 files changed
+649 -3

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
import { test, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import { config } from '../server/config.js'
import { zeitenAus, zeitenFuerKunde, zeitenModus } from '../server/services/zeiten/schalter.js'
const ZEITEN_ENV = [
'ZEITEN_ENABLED',
'ZEITEN_PILOT_EMAILS',
'ZEITEN_KEIN_NEUBAU',
'APPWRITE_COLLECTION_OEFFNUNGSZEITEN',
'APPWRITE_COLLECTION_ZEITEN_ERINNERUNGEN',
]
let original
beforeEach(() => {
original = { ...config.zeiten }
})
afterEach(() => {
Object.assign(config.zeiten, original)
})
/** Frische config.js-Instanz mit genau diesen Zeiten-Umgebungsvariablen (alle anderen entfernt). */
let freshCounter = 0
async function freshConfig(env = {}) {
const saved = Object.fromEntries(ZEITEN_ENV.map((key) => [key, process.env[key]]))
for (const key of ZEITEN_ENV) delete process.env[key]
Object.assign(process.env, env)
try {
freshCounter += 1
return (await import(`../server/config.js?zeiten-test=${freshCounter}`)).config
} finally {
for (const key of ZEITEN_ENV) {
if (saved[key] === undefined) delete process.env[key]
else process.env[key] = saved[key]
}
}
}
test('Vorgabe ist aus', async () => {
const fresh = await freshConfig()
assert.deepEqual(fresh.zeiten, { mode: 'off', pilotEmails: [], keinNeubau: ['emmas-schatzkiste'] })
// ZEITEN_ENABLED leer ⇒ aus; der Schalter liest config.zeiten bei jedem Aufruf
config.zeiten.mode = fresh.zeiten.mode
assert.equal(zeitenModus(), 'off')
assert.equal(zeitenAus(), true)
assert.equal(zeitenFuerKunde('kenso.gri@gmail.com'), false)
})
test('Konfiguration: leerer oder unbekannter Schalter ist off, Umgebungsvariablen werden bereinigt', async () => {
for (const value of ['', ' ', 'banane', 'true', '1']) {
assert.equal((await freshConfig({ ZEITEN_ENABLED: value })).zeiten.mode, 'off', `ZEITEN_ENABLED=${JSON.stringify(value)}`)
}
assert.equal((await freshConfig({ ZEITEN_ENABLED: ' ON ' })).zeiten.mode, 'on')
const fresh = await freshConfig({
ZEITEN_ENABLED: ' Pilot ',
ZEITEN_PILOT_EMAILS: ' Kenso.Gri@Gmail.com, ,justin@webklar.com ',
})
assert.equal(fresh.zeiten.mode, 'pilot')
assert.deepEqual(fresh.zeiten.pilotEmails, ['kenso.gri@gmail.com', 'justin@webklar.com'])
})
test('zeitenModus: leer und unbekannt sind off, pilot und on bleiben', () => {
for (const value of [undefined, null, '', 'banane', 'PILOT', 'true']) {
config.zeiten.mode = value
assert.equal(zeitenModus(), 'off', `mode=${JSON.stringify(value)}`)
assert.equal(zeitenAus(), true)
}
config.zeiten.mode = 'pilot'
assert.equal(zeitenModus(), 'pilot')
assert.equal(zeitenAus(), false)
config.zeiten.mode = 'on'
assert.equal(zeitenModus(), 'on')
assert.equal(zeitenAus(), false)
})
test('off sperrt alle, auch mit Stern in der Liste', () => {
config.zeiten.mode = 'off'
config.zeiten.pilotEmails = ['*', 'kenso.gri@gmail.com']
assert.equal(zeitenFuerKunde('kenso.gri@gmail.com'), false)
assert.equal(zeitenFuerKunde('jemand@example.com'), false)
})
test('pilot gilt nur für die Pilotliste', () => {
config.zeiten.mode = 'pilot'
config.zeiten.pilotEmails = ['a@b.de']
assert.equal(zeitenFuerKunde('A@b.de'), true)
assert.equal(zeitenFuerKunde(' a@b.de '), true)
assert.equal(zeitenFuerKunde('x@y.de'), false)
assert.equal(zeitenFuerKunde('a@b.de.evil.de'), false)
// Stern = alle, leere Liste = niemand, ohne Adresse = niemand
config.zeiten.pilotEmails = ['*']
assert.equal(zeitenFuerKunde('x@y.de'), true)
config.zeiten.pilotEmails = []
assert.equal(zeitenFuerKunde('a@b.de'), false)
config.zeiten.pilotEmails = ['a@b.de']
for (const email of [undefined, null, '', ' ']) assert.equal(zeitenFuerKunde(email), false)
})
test('on gilt für alle', () => {
config.zeiten.mode = 'on'
config.zeiten.pilotEmails = ['a@b.de']
assert.equal(zeitenFuerKunde('x@y.de'), true)
assert.equal(zeitenFuerKunde(undefined), true)
})
test('keinNeubau kommt aus ZEITEN_KEIN_NEUBAU, Vorgabe emmas-schatzkiste', async () => {
assert.deepEqual((await freshConfig()).zeiten.keinNeubau, ['emmas-schatzkiste'])
assert.deepEqual((await freshConfig({ ZEITEN_KEIN_NEUBAU: '' })).zeiten.keinNeubau, ['emmas-schatzkiste'])
assert.deepEqual(
(await freshConfig({ ZEITEN_KEIN_NEUBAU: ' Emmas-Schatzkiste, , Andere-Site ' })).zeiten.keinNeubau,
['emmas-schatzkiste', 'andere-site'],
)
})
test('zeiten steht neben config.termine, nicht darin', async () => {
const fresh = await freshConfig()
assert.equal('keinNeubau' in fresh.termine, false)
assert.equal('zeiten' in fresh.termine, false)
assert.notEqual(fresh.zeiten, fresh.termine)
})
test('zeitenSetup: 3000 Cent einmalig', async () => {
const fresh = await freshConfig()
assert.deepEqual(fresh.zeitenSetup, {
key: 'zeitenSetup',
name: 'Öffnungszeiten live – Einrichtung',
amountCents: 3000,
lookupKey: 'webklar_zeiten_einrichtung_3000',
oneTime: true,
priceNickname: 'Öffnungszeiten-Einrichtung 30 €',
})
assert.equal(config.zeitenSetup.amountCents, 3000)
assert.equal(config.zeitenSetup.oneTime, true)
assert.equal(config.zeitenSetup.lookupKey, 'webklar_zeiten_einrichtung_3000')
})
test('Konfiguration: Collection-IDs der Zeiten-Collections', async () => {
const fresh = await freshConfig()
assert.equal(fresh.collections.oeffnungszeiten, 'oeffnungszeiten')
assert.equal(fresh.collections.zeitenErinnerungen, 'zeitenErinnerungen')
const anders = await freshConfig({
APPWRITE_COLLECTION_OEFFNUNGSZEITEN: 'zDaten',
APPWRITE_COLLECTION_ZEITEN_ERINNERUNGEN: 'zMerker',
})
assert.deepEqual([anders.collections.oeffnungszeiten, anders.collections.zeitenErinnerungen], ['zDaten', 'zMerker'])
})