Ö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:
ClaudeandClaude Opus 5.5 committed 2026-10-04 23:31:36 +00:00
1 parent 2314f7d940
commit db9f7df747
12 files changed
+319 -18

No files matched your search

+66
View File
@@ -283,6 +283,72 @@ const features = {
orderedConflictMessage: 'Der Terminplaner ist für dieses Projekt schon bestellt.',
unavailableMessage: 'Der Terminplaner ist noch nicht verfügbar.',
},
// Oeffnungszeiten live (Spec 2026-10-04): 30 EUR einmalig, im Wartungs-Abo ohne Aufpreis (aboInklusive, consentFrei)
zeiten: {
key: 'zeiten',
product: 'zeitenSetup',
label: { name: 'Öffnungszeiten live', kurz: 'Zeiten', keine: 'keine Öffnungszeiten live' },
priceLabel: '30 €',
setupEstimate: 'meist innerhalb weniger Tage',
// Wortlaut der Zustimmung (bezahlter Weg); wird mit Zeitstempel am Feature-Dokument gespeichert (Widerruf, § 356 Abs. 5 Nr. 2 BGB)
consent: {
version: '2026-10-04',
knopf: 'Zahlungspflichtig bestellen · 30 €',
items: [
{
id: 'leistung',
haken: true,
text: 'Ich habe die Leistungsbeschreibung „Öffnungszeiten live“ gelesen und bestelle die Einrichtung.',
},
{
id: 'beginn',
haken: true,
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).',
},
],
},
// Zustimmung des Gratisweges (Wartungs-Abo laeuft): ein Haken, kein Widerrufsverlangen, kein Preis
consentFrei: {
version: '2026-10-04',
knopf: 'Einschalten',
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.',
},
],
},
leistungsbeschreibung: { version: '2026-10-04', url: '/vertragsunterlagen/zeiten-leistungsbeschreibung.html' },
avv: null,
widerruf: null,
returnHash: 'zeiten',
returnProjekt: true,
ticket: {
topic: (project) => `Öffnungszeiten live einbauen – ${ticketProjektName(project)}`.trim(),
// Dienst erst beim Aufruf laden: config.js importiert keine Dienste statisch
details: async (ctx) => (await import('./services/zeiten/ticketDetails.js')).zeitenTicketDetails(ctx),
},
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,
},
requiresWartungsAbo: false,
// Im Wartungs-Abo ohne Aufpreis enthalten (Gratisweg POST /api/funktionen/zeiten/einschalten)
aboInklusive: true,
ticketFreischaltung: true,
// Die oeffentliche Schnittstelle liefert in diesen Zustaenden dasselbe (Ersatz-Tabelle bis zur Freischaltung)
teilweiseLive: ['setup', 'active'],
disabledMessage:
'Öffnungszeiten live wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du die Funktion wieder möchtest.',
orderedConflictMessage: 'Öffnungszeiten live ist für dieses Projekt schon bestellt oder eingeschaltet.',
unavailableMessage: 'Öffnungszeiten live ist noch nicht verfügbar.',
},
}
export const config = {
+19 -2
View File
@@ -1,9 +1,10 @@
import { config } from '../config.js'
import { termineFuerKunde } from './termine/schalter.js'
import { zeitenFuerKunde } from './zeiten/schalter.js'
// Helfer fuer das Funktionen-Register config.features (News-Pinnwand, Terminplaner; Daten in server/config.js).
// Helfer fuer das Funktionen-Register config.features (News-Pinnwand, Terminplaner, Oeffnungszeiten live; Daten in server/config.js).
// Diese Datei wird von siteFeatures.js importiert und importiert selbst keine Dienste statisch (einzige Ausnahme:
// termine/schalter.js, das nur config liest): Standard-Haken laden ihre Dienste per dynamischem import()
// termine/schalter.js und zeiten/schalter.js, die nur config lesen): Standard-Haken laden ihre Dienste per dynamischem import()
// innerhalb der Funktion (sonst entstuenden Importzyklen).
/** Namen der Haken, die ein Registereintrag hat (optional): Bestell-, Freischalt- und Unterlagenpruefungen. */
@@ -58,6 +59,14 @@ const STANDARD_HAKEN = {
belehrung: async () => BELEHRUNG_PLATZHALTER,
freischaltbar: termineFreischaltbar,
},
// Oeffnungszeiten live: keine Bestellpruefung (die Einstellung entsteht erst im Editor); belehrung und
// unterlagenBereit wie beim Terminplaner; freischaltbar ist ein Platzhalter (Task 18 ersetzt ihn)
zeiten: {
verfuegbar: (email) => zeitenFuerKunde(email),
unterlagenBereit: async () => true,
belehrung: async () => BELEHRUNG_PLATZHALTER,
freischaltbar: async () => true,
},
}
/** Registereintrag oder null; nur eigene Schluessel des Registers ('analyse', '__proto__', 'toString' => null). */
@@ -112,6 +121,14 @@ export function featureSperre(key, email) {
return eintrag.unavailableMessage || NICHT_VERFUEGBAR
}
/**
* Gratisweg einer Funktion mit aboInklusive (Wartungs-Abo laeuft): derselbe Eintrag, nur mit der Zustimmung consentFrei
* (ein Haken, Knopf "Einschalten") statt consent. Eine Kopie, das Register bleibt unveraendert.
*/
export function freiweg(feature) {
return { ...feature, consent: feature.consentFrei }
}
const fassung = (f) => (f ? { version: f.version, url: f.url ?? '' } : null)
/**
+3 -1
View File
@@ -58,6 +58,7 @@ const PRODUCT_METADATA = {
mailStorage: 'mail-speicher',
newsSetup: 'news-einrichtung',
termineSetup: 'termine-einrichtung',
zeitenSetup: 'zeiten-einrichtung',
}
/** Katalogeintrag zum Schluessel: die beiden Abos aus products.js, Zusatzspeicher und Einrichtungen aus config. */
@@ -65,13 +66,14 @@ function catalogFor(key) {
if (key === 'mailStorage') return config.mailStorage
if (key === 'newsSetup') return config.newsSetup
if (key === 'termineSetup') return config.termineSetup
if (key === 'zeitenSetup') return config.zeitenSetup
return getProduct(key)
}
const priceIdCache = new Map()
/**
* Preis per lookup_key holen oder (einmalig) samt Produkt anlegen. key: 'wartung' | 'hosting' |
* 'mailStorage' | 'newsSetup' | 'termineSetup' (Einmalpreise). Ersetzt das fruehere ensureAboPrice (nur Wartung); lookup_keys werden bei Stripe
* 'mailStorage' | 'newsSetup' | 'termineSetup' | 'zeitenSetup' (Einmalpreise). Ersetzt das fruehere ensureAboPrice (nur Wartung); lookup_keys werden bei Stripe
* nie uebertragen, ein neuer Preis bekaeme also ohnehin einen neuen lookup_key im Katalog.
*/
export async function ensurePrice(key) {
+10
View File
@@ -0,0 +1,10 @@
import { minimaleTicketDetails } from '../featureRegistry.js'
/**
* Details des Einrichtungs-Tickets von Oeffnungszeiten live. Vorlaeufig die Notfall-Details des Registers (Kopfzeilen und
* Verweis auf die Anleitung im WOMS); Task 14 baut daraus die vollstaendige Anleitung.
* ctx = { feature, doc, project, customer }.
*/
export async function zeitenTicketDetails(ctx) {
return minimaleTicketDetails(ctx)
}