Ö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:
1 parent
385f768d76
commit
4b4cd1058f
7 files changed
+649
-3
No files matched your search
@@ -30,6 +30,16 @@ function nonNegativeNumber(raw, fallback) {
|
||||
}
|
||||
|
||||
const termineEnabled = (process.env.TERMINE_ENABLED || '').trim().toLowerCase()
|
||||
const zeitenEnabled = (process.env.ZEITEN_ENABLED || '').trim().toLowerCase()
|
||||
|
||||
/** Komma-Liste aus einer Umgebungsvariablen: getrimmt, klein geschrieben, ohne Leereintraege; leer ergibt den Vorgabewert. */
|
||||
function commaList(raw, fallback) {
|
||||
const list = String(raw || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
return list.length ? list : fallback
|
||||
}
|
||||
|
||||
// Abo-Produkte (Spec 2026-09-27 "Verträge, Hosting-Abo und E-Mail-Zusatzspeicher"). Preise in Cent,
|
||||
// Endpreise gemäß § 19 UStG. Texte, die Kunden sehen, stimmen mit den Vertragsbedingungen überein.
|
||||
@@ -116,6 +126,16 @@ const termineSetup = {
|
||||
priceNickname: 'Terminplaner-Einrichtung 149 €',
|
||||
}
|
||||
|
||||
// Oeffnungszeiten live: einmalige Einrichtung (Einmalpreis, kein Abo); im Wartungs-Abo ist die Funktion enthalten
|
||||
const zeitenSetup = {
|
||||
key: 'zeitenSetup',
|
||||
name: 'Öffnungszeiten live – Einrichtung',
|
||||
amountCents: 3000,
|
||||
lookupKey: 'webklar_zeiten_einrichtung_3000',
|
||||
oneTime: true,
|
||||
priceNickname: 'Öffnungszeiten-Einrichtung 30 €',
|
||||
}
|
||||
|
||||
// Ticketsystem (WOMS); eigene Konstante, weil auch die Enable-Banking-Rueckkehradresse darauf aufbaut
|
||||
const womsBaseUrl = (process.env.WOMS_PUBLIC_URL || 'https://ticket.webklar.com').replace(/\/$/, '')
|
||||
|
||||
@@ -310,6 +330,9 @@ export const config = {
|
||||
termineBookings: process.env.APPWRITE_COLLECTION_TERMINE_BOOKINGS || 'termineBookings',
|
||||
termineClaims: process.env.APPWRITE_COLLECTION_TERMINE_CLAIMS || 'termineClaims',
|
||||
termineOutbox: process.env.APPWRITE_COLLECTION_TERMINE_OUTBOX || 'termineOutbox',
|
||||
// Oeffnungszeiten live (Einstellungen, Erinnerungsmerker; scripts/zeiten-setup.mjs)
|
||||
oeffnungszeiten: process.env.APPWRITE_COLLECTION_OEFFNUNGSZEITEN || 'oeffnungszeiten',
|
||||
zeitenErinnerungen: process.env.APPWRITE_COLLECTION_ZEITEN_ERINNERUNGEN || 'zeitenErinnerungen',
|
||||
},
|
||||
|
||||
publicBaseUrl: (process.env.PORTAL_PUBLIC_URL || 'https://project.webklar.com').replace(/\/$/, ''),
|
||||
@@ -326,6 +349,7 @@ export const config = {
|
||||
mailStorage,
|
||||
newsSetup,
|
||||
termineSetup,
|
||||
zeitenSetup,
|
||||
ustHinweis,
|
||||
// Register der kaufbaren Funktionen (siehe oben)
|
||||
features,
|
||||
@@ -551,6 +575,18 @@ export const config = {
|
||||
minAntwortMs: nonNegativeNumber(process.env.TERMINE_MIN_ANTWORT_MS, 300),
|
||||
},
|
||||
|
||||
// Oeffnungszeiten live (Funktionsschluessel 'zeiten'); Geschwister von termine (termineSchalter.test.js pinnt
|
||||
// config.termine). Wird bei jedem Aufruf gelesen; Schalter siehe server/services/zeiten/schalter.js
|
||||
zeiten: {
|
||||
// off (Vorgabe) | pilot (nur ZEITEN_PILOT_EMAILS) | on (alle Kunden)
|
||||
mode: ['pilot', 'on'].includes(zeitenEnabled) ? zeitenEnabled : 'off',
|
||||
// Kunden-E-Mails fuer den Pilotbetrieb; '*' = alle
|
||||
pilotEmails: commaList(process.env.ZEITEN_PILOT_EMAILS, []),
|
||||
// Subdomains, fuer die der Neubau nie angestossen wird (Einbau von Hand); ZEITEN_KEIN_NEUBAU ist eine
|
||||
// Komma-Liste, die Vorgabe ist emmas-schatzkiste
|
||||
keinNeubau: commaList(process.env.ZEITEN_KEIN_NEUBAU, ['emmas-schatzkiste']),
|
||||
},
|
||||
|
||||
invoiceNinja: {
|
||||
url: (process.env.INVOICE_NINJA_URL || 'https://invoice.webklar.com').replace(/\/$/, ''),
|
||||
apiToken: process.env.INVOICE_NINJA_API_TOKEN || '',
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { config } from '../../config.js'
|
||||
|
||||
/**
|
||||
* Oeffnungszeiten-Schalter (Funktionsschluessel 'zeiten'): ZEITEN_ENABLED = off (Vorgabe) | pilot | on.
|
||||
* Liest config.zeiten bei jedem Aufruf. Bei off laufen keine Zeiten-Jobs und keine Zeiten-Abfragen.
|
||||
*/
|
||||
|
||||
/** 'off' | 'pilot' | 'on'; alles andere (leer, unbekannt) gilt als off. */
|
||||
export function zeitenModus() {
|
||||
const mode = config.zeiten.mode
|
||||
return mode === 'pilot' || mode === 'on' ? mode : 'off'
|
||||
}
|
||||
|
||||
export function zeitenAus() {
|
||||
return zeitenModus() === 'off'
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf dieser Kunde Oeffnungszeiten live nutzen? pilot = nur Adressen aus ZEITEN_PILOT_EMAILS ('*' = alle; eine
|
||||
* leere Liste laesst niemanden durch); on = alle Kunden; off = niemand.
|
||||
*/
|
||||
export function zeitenFuerKunde(email) {
|
||||
const modus = zeitenModus()
|
||||
if (modus === 'off') return false
|
||||
if (modus === 'on') return true
|
||||
const erlaubt = config.zeiten.pilotEmails
|
||||
if (erlaubt.includes('*')) return true
|
||||
const adresse = String(email || '').trim().toLowerCase()
|
||||
return adresse !== '' && erlaubt.includes(adresse)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Oeffnungszeiten live: Appwrite-Schema der beiden Collections und der neuen portalFeatures-Felder
|
||||
* (Spezifikation 2026-10-04-oeffnungszeiten-live-design.md, Abschnitte 2.1 bis 2.3).
|
||||
* Reine Daten, absichtlich ohne Import von server/config.js: test/support/schema.js liest sie vor useTestEnv(),
|
||||
* ein frueh geladenes config.js wuerde den Endpunkt des Appwrite-Nachbaus verfehlen. Die Collection-IDs stehen in
|
||||
* config.collections.<Name>; scripts/zeiten-setup.mjs legt alles an (auch die portalFeatures-Felder, bewusst nicht
|
||||
* scripts/feature-setup.mjs: dessen Tests pinnen genau drei Attribute).
|
||||
*
|
||||
* Attribut: { key, type: 'string' | 'integer' | 'boolean' | 'datetime', size (nur Text), required: false }
|
||||
* Index: { key, type: 'key', attributes: [...] }
|
||||
* Datumswerte sind UTC (toISOString()); Appwrite liefert sie als "+00:00" zurueck, daher immer mit new Date() lesen.
|
||||
*/
|
||||
|
||||
const str = (key, size) => ({ key, type: 'string', size, required: false })
|
||||
const int = (key) => ({ key, type: 'integer', required: false })
|
||||
const dt = (key) => ({ key, type: 'datetime', required: false })
|
||||
const index = (key, ...attributes) => ({ key, type: 'key', attributes })
|
||||
|
||||
export const ZEITEN_COLLECTIONS = {
|
||||
// Eine Einstellung je Funktion; Dokument-ID = ID des portalFeatures-Dokuments. Nur der Server-Key schreibt.
|
||||
// daten = JSON der Kundeneingaben (hoechstens 32 KB, die Spalte ist groesser), team = JSON der Team-Einstellungen
|
||||
// (nur ueber WOMS aenderbar), ersatzHash = Hash des Hauptplan-Wochenplans beim letzten Ersatz-HTML,
|
||||
// updatedBy = 'kunde' | 'team:<staffId>'.
|
||||
oeffnungszeiten: {
|
||||
attributes: [
|
||||
str('customerId', 64),
|
||||
str('projectId', 64),
|
||||
str('featureId', 64),
|
||||
int('version'),
|
||||
str('daten', 40000),
|
||||
int('teamVersion'),
|
||||
str('team', 2000),
|
||||
str('ersatzHash', 64),
|
||||
dt('updatedAt'),
|
||||
str('updatedBy', 64),
|
||||
],
|
||||
// idx_customer: Loeschung beim Kunden-Loeschen
|
||||
indexes: [index('idx_customer', 'customerId')],
|
||||
},
|
||||
|
||||
// Erinnerungsmerker der Feiertags-Mail; Dokument-ID = sha1(featureId|datum) gekuerzt (anlegen mit 409 = schon
|
||||
// erinnert). datum = Kalendertag 'YYYY-MM-DD' (Berliner Zeit); der Tagesjob loescht Merker, deren Datum > 30 Tage
|
||||
// zurueckliegt (idx_datum).
|
||||
zeitenErinnerungen: {
|
||||
attributes: [str('featureId', 64), str('datum', 10), dt('createdAt')],
|
||||
indexes: [index('idx_datum', 'datum')],
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Neue Felder von portalFeatures (alle optional), angelegt von scripts/zeiten-setup.mjs:
|
||||
* - unlockedBy: 'kauf' | 'abo' | 'team'
|
||||
* - aboEndeAt: Zeitpunkt, an dem der Abgleich das Ende des Wartungs-Abos erkannt hat
|
||||
* - einbau: 'angestossen' | 'manuell' (Ergebnis des Neubau-Anstosses)
|
||||
* - behaltenSessionId: offene Behalten-Zahlung (Stripe-Sitzung)
|
||||
* - k24At: Marker der Mail K24 (Selbstheilung)
|
||||
*/
|
||||
export const ZEITEN_FEATURE_ATTRIBUTES = [
|
||||
str('unlockedBy', 8),
|
||||
dt('aboEndeAt'),
|
||||
str('einbau', 12),
|
||||
str('behaltenSessionId', 128),
|
||||
dt('k24At'),
|
||||
]
|
||||
|
||||
/** Datumsfelder je Collection, damit der Appwrite-Nachbau der Tests sie wie Appwrite formatieren kann. */
|
||||
export const ZEITEN_DATETIMES = Object.fromEntries(
|
||||
Object.entries(ZEITEN_COLLECTIONS).map(([name, { attributes }]) => [
|
||||
name,
|
||||
attributes.filter((attribute) => attribute.type === 'datetime').map((attribute) => attribute.key),
|
||||
]),
|
||||
)
|
||||
Reference in new issue
Block a user