Öffnungszeiten: Register-Eintrag und Stripe-Katalog
Funktion zeiten in config.features (30 €, aboInklusive, consentFrei, teilweiseLive), freiweg() und Standard-Haken in featureRegistry.js, Stripe-Katalog zeitenSetup (Metadaten zeiten-einrichtung) in stripeBilling.js und scripts/stripe-products.mjs, vorläufige zeitenTicketDetails; Schlüssellisten-Pins um zeiten ergänzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2314f7d940
commit
db9f7df747
12 files changed
+319
-18
No files matched your search
@@ -103,15 +103,15 @@ afterEach(() => {
|
||||
|
||||
// ---------------------------------------------------------------------------------------- Register ---
|
||||
test('ticketFreischaltungKeys: Register-Schluessel mit ticketFreischaltung, in Registerreihenfolge', () => {
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine'])
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'zeiten'])
|
||||
NEWS.ticketFreischaltung = false
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine'])
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine', 'zeiten'])
|
||||
NEWS.ticketFreischaltung = 'ja'
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine'], 'nur true zaehlt')
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['termine', 'zeiten'], 'nur true zaehlt')
|
||||
NEWS.ticketFreischaltung = true
|
||||
TERMINE.ticketFreischaltung = false
|
||||
try {
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news'])
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'zeiten'])
|
||||
} finally {
|
||||
TERMINE.ticketFreischaltung = true
|
||||
}
|
||||
|
||||
@@ -120,10 +120,11 @@ 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' },
|
||||
zeiten: { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' },
|
||||
})
|
||||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine'])
|
||||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'zeiten'])
|
||||
assert.deepEqual(sf.FEATURE_KEYS, reg.featureKeys())
|
||||
assert.deepEqual(reg.featureKeys(), ['news', 'termine'])
|
||||
assert.deepEqual(reg.featureKeys(), ['news', 'termine', 'zeiten'])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------- Schlüsselschutz ---
|
||||
|
||||
@@ -69,7 +69,7 @@ test('liste_zustaende', async () => {
|
||||
assert.deepEqual(body.termine, { verfuegbar: false })
|
||||
|
||||
// Register-Ansicht (B12): Preise und Bestelltexte beider Funktionen aus config.features, auch bei termine.verfuegbar: false
|
||||
assert.deepEqual(Object.keys(body.features).sort(), ['news', 'termine'])
|
||||
assert.deepEqual(Object.keys(body.features).sort(), ['news', 'termine', 'zeiten'])
|
||||
assert.equal(body.features.news.priceLabel, '30 €')
|
||||
assert.equal(body.features.termine.priceLabel, '149 €')
|
||||
assert.equal(body.ustHinweis, config.ustHinweis)
|
||||
|
||||
@@ -246,7 +246,7 @@ test('GET /api/funktionen: Projekte tragen features.termine und News wie bisher'
|
||||
assert.equal(pt1.features.news.state, 'disabled')
|
||||
const pt2 = body.projects.find((p) => p.projectId === 'pt2')
|
||||
assert.deepEqual([pt2.features.termine.state, pt2.features.news.state], ['off', 'off'])
|
||||
assert.deepEqual(Object.keys(pt2.features), ['news', 'termine'])
|
||||
assert.deepEqual(Object.keys(pt2.features), ['news', 'termine', 'zeiten'])
|
||||
// Abschaltgrund und Loeschfrist (E2, R105): News ist hier vom Team abgeschaltet: Grund und Zeitpunkt ja, ein Loeschdatum nein
|
||||
// (der 30-Tage-Loeschlauf loescht nur Terminplaner-Daten); der aktive Terminplaner und das leere Projekt haben nichts
|
||||
const aus = pt1.features.news
|
||||
|
||||
@@ -170,17 +170,18 @@ test('termine_ist_zweite_funktion: Schluessel und Bezeichnungen', () => {
|
||||
aw.seed('websiteProjects', { $id: 'pt1', customerId: 'c1', projectName: 'Salon' })
|
||||
aw.seed('websiteProjects', { $id: 'pt2', customerId: 'c1', projectName: 'Studio' })
|
||||
aw.seed('websiteProjects', { $id: 'pt3', customerId: 'c2', projectName: 'Fremder Salon' })
|
||||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine'])
|
||||
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'zeiten'])
|
||||
assert.deepEqual(sf.FEATURE_LABELS, {
|
||||
news: { name: 'News-Pinnwand', keine: 'keine News-Pinnwand' },
|
||||
termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' },
|
||||
zeiten: { name: 'Öffnungszeiten live', keine: 'keine Öffnungszeiten live' },
|
||||
})
|
||||
})
|
||||
|
||||
test('liste_zeigt_termine_je_projekt_ohne_dokument_als_off', async () => {
|
||||
const list = await sf.listCustomerFeatures('c1')
|
||||
const p = list.find((x) => x.projectId === 'pt1')
|
||||
assert.deepEqual(Object.keys(p.features), ['news', 'termine'])
|
||||
assert.deepEqual(Object.keys(p.features), ['news', 'termine', 'zeiten'])
|
||||
assert.equal(p.features.termine.state, 'off')
|
||||
assert.equal(p.features.termine.siteKey, null)
|
||||
assert.equal(p.features.termine.ticketWoid, null)
|
||||
@@ -246,7 +247,7 @@ test('activateClosedTickets: Funktionen mit ticketFreischaltung; News verhaelt s
|
||||
const ids = ['f_setup_termine', 'f_setup_news', 'f_setup_analyse']
|
||||
const reg = await import('../server/services/featureRegistry.js')
|
||||
try {
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine'])
|
||||
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'zeiten'])
|
||||
// News wie bisher; Termine wartet auf Einstellung und Abo (Haken freischaltbar); Analyse hat keine ticketFreischaltung
|
||||
const done = await sf.activateClosedTickets({ customerId: 'c1' })
|
||||
assert.deepEqual(done.map((d) => d.featureKey), ['news'])
|
||||
|
||||
@@ -31,12 +31,13 @@ test('Import startet main() nicht: kein Stripe-Aufruf, kein process.exit', () =>
|
||||
assert.equal(st.calls.length, 0)
|
||||
})
|
||||
|
||||
test('KEYS enthaelt den Terminplaner-Katalogschluessel', () => {
|
||||
assert.deepEqual(script.KEYS, ['wartung', 'hosting', 'mailStorage', 'newsSetup', 'termineSetup'])
|
||||
test('KEYS enthaelt den Terminplaner- und den Oeffnungszeiten-Katalogschluessel', () => {
|
||||
assert.deepEqual(script.KEYS, ['wartung', 'hosting', 'mailStorage', 'newsSetup', 'termineSetup', 'zeitenSetup'])
|
||||
})
|
||||
|
||||
test('catalogEntry liefert die Katalogeintraege aus der Konfiguration', () => {
|
||||
assert.equal(script.catalogEntry('termineSetup'), config.termineSetup)
|
||||
assert.equal(script.catalogEntry('zeitenSetup'), config.zeitenSetup)
|
||||
assert.equal(script.catalogEntry('newsSetup'), config.newsSetup)
|
||||
assert.equal(script.catalogEntry('mailStorage'), config.mailStorage)
|
||||
assert.equal(script.catalogEntry('wartung'), config.products.wartung)
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
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 wie die Notfall-Details', 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.equal(details, reg.minimaleTicketDetails(ctx))
|
||||
assert.match(details, /^Funktion: zeiten$/m)
|
||||
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)
|
||||
})
|
||||
Reference in new issue
Block a user