Buchungskalender: Konfiguration, Schalter, Schema und Setup-Skript
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
18369a348c
commit
5717a5cdd0
7 files changed
+728
No files matched your search
@@ -31,6 +31,7 @@ function nonNegativeNumber(raw, fallback) {
|
||||
|
||||
const termineEnabled = (process.env.TERMINE_ENABLED || '').trim().toLowerCase()
|
||||
const shopEnabled = (process.env.SHOP_ENABLED || '').trim().toLowerCase()
|
||||
const unterkunftEnabled = (process.env.UNTERKUNFT_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. */
|
||||
@@ -137,6 +138,16 @@ const shopSetup = {
|
||||
priceNickname: 'Webshop-Einrichtung 299 €',
|
||||
}
|
||||
|
||||
// Buchungskalender (Funktionsschluessel 'unterkunft'): einmalige Einrichtung (Einmalpreis, kein Abo)
|
||||
const unterkunftSetup = {
|
||||
key: 'unterkunftSetup',
|
||||
name: 'Buchungskalender (Einrichtung)',
|
||||
amountCents: 19900,
|
||||
lookupKey: 'webklar_unterkunft_einrichtung_19900',
|
||||
oneTime: true,
|
||||
priceNickname: 'Buchungskalender-Einrichtung 199 €',
|
||||
}
|
||||
|
||||
// Oeffnungszeiten live: einmalige Einrichtung (Einmalpreis, kein Abo); im Wartungs-Abo ist die Funktion enthalten
|
||||
const zeitenSetup = {
|
||||
key: 'zeitenSetup',
|
||||
@@ -631,6 +642,13 @@ export const config = {
|
||||
shopOrders: process.env.APPWRITE_COLLECTION_SHOP_ORDERS || 'shopOrders',
|
||||
shopWiderrufe: process.env.APPWRITE_COLLECTION_SHOP_WIDERRUFE || 'shopWiderrufe',
|
||||
shopRechtstexteArchiv: process.env.APPWRITE_COLLECTION_SHOP_RECHTSTEXTE_ARCHIV || 'shopRechtstexteArchiv',
|
||||
// Buchungskalender (Unterkuenfte, Preise, Sperren, Anfragen, Rechnungen, Einstellungen; scripts/unterkunft-setup.mjs)
|
||||
ukUnterkuenfte: process.env.APPWRITE_COLLECTION_UK_UNTERKUENFTE || 'ukUnterkuenfte',
|
||||
ukPreise: process.env.APPWRITE_COLLECTION_UK_PREISE || 'ukPreise',
|
||||
ukSperren: process.env.APPWRITE_COLLECTION_UK_SPERREN || 'ukSperren',
|
||||
ukAnfragen: process.env.APPWRITE_COLLECTION_UK_ANFRAGEN || 'ukAnfragen',
|
||||
ukRechnungen: process.env.APPWRITE_COLLECTION_UK_RECHNUNGEN || 'ukRechnungen',
|
||||
ukEinstellungen: process.env.APPWRITE_COLLECTION_UK_EINSTELLUNGEN || 'ukEinstellungen',
|
||||
// Oeffnungszeiten live (Einstellungen, Erinnerungsmerker; scripts/zeiten-setup.mjs)
|
||||
oeffnungszeiten: process.env.APPWRITE_COLLECTION_OEFFNUNGSZEITEN || 'oeffnungszeiten',
|
||||
zeitenErinnerungen: process.env.APPWRITE_COLLECTION_ZEITEN_ERINNERUNGEN || 'zeitenErinnerungen',
|
||||
@@ -651,6 +669,7 @@ export const config = {
|
||||
newsSetup,
|
||||
termineSetup,
|
||||
shopSetup,
|
||||
unterkunftSetup,
|
||||
zeitenSetup,
|
||||
ustHinweis,
|
||||
// Register der kaufbaren Funktionen (siehe oben)
|
||||
@@ -915,6 +934,47 @@ export const config = {
|
||||
unbezahltLoeschTage: 30,
|
||||
},
|
||||
|
||||
// Buchungskalender (Funktionsschluessel 'unterkunft'). Wird bei jedem Aufruf gelesen (Tests aendern die Werte zur
|
||||
// Laufzeit); Schalter siehe server/services/unterkunft/schalter.js
|
||||
unterkunft: {
|
||||
// off (Vorgabe) | pilot (nur UNTERKUNFT_PILOT_EMAILS) | on (alle Kunden)
|
||||
mode: ['pilot', 'on'].includes(unterkunftEnabled) ? unterkunftEnabled : 'off',
|
||||
// Kunden-E-Mails fuer den Pilotbetrieb; '*' = alle
|
||||
pilotEmails: (process.env.UNTERKUNFT_PILOT_EMAILS || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
// Fassung der Rechtstexte: Berliner Kalendertag (JJJJ-MM-TT); eine ausgelieferte Fassung wird nie ueberschrieben
|
||||
fassung: '2026-10-07',
|
||||
// Mindestzeit (Sekunden) zwischen Laden und Absenden des Anfrageformulars (schneller = Bot)
|
||||
formMinSekunden: nonNegativeNumber(process.env.UNTERKUNFT_FORM_MIN_SEKUNDEN, 3),
|
||||
// Kuerzeste Antwortzeit (ms) der POST-Routen der oeffentlichen Schnittstelle. Tests setzen 0.
|
||||
minAntwortMs: nonNegativeNumber(process.env.UNTERKUNFT_MIN_ANTWORT_MS, 300),
|
||||
// iCal-Import: Abrufintervall, Groessen-/Zeitgrenzen, Fehlermail nach so vielen Stunden anhaltenden Fehlern
|
||||
icalIntervallMin: 15,
|
||||
icalMaxBytes: 1_000_000,
|
||||
icalTimeoutMs: 10_000,
|
||||
icalFehlerStunden: 6,
|
||||
// Buchbar sind so viele Monate in die Zukunft
|
||||
horizontMonate: 18,
|
||||
maxPosten: 30,
|
||||
postenTextMax: 200,
|
||||
nachrichtMax: 2000,
|
||||
// Anfragen je Unterkunfts-Seite und Stunde (Missbrauchsbremse)
|
||||
anfragenJeSiteStunde: 30,
|
||||
// IP-Hashes werden nach so vielen Tagen geloescht
|
||||
ipHashTage: 30,
|
||||
// Unverbindliche Anfragen werden nach so vielen Monaten anonymisiert/geloescht
|
||||
unverbindlichMonate: 6,
|
||||
// Offene (neue) Anfragen verfallen nach so vielen Tagen
|
||||
neuVerfallTage: 60,
|
||||
// Anfragen werden so viele Jahre nach Abschluss anonymisiert; Rechnungen bleiben rechnungJahre Jahre
|
||||
anonymisierenJahre: 3,
|
||||
rechnungJahre: 8,
|
||||
// Tage nach Abschalten der Funktion bis zur Loeschung der Daten
|
||||
loeschTageNachAbschalten: 90,
|
||||
},
|
||||
|
||||
// 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: {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { config } from '../../config.js'
|
||||
|
||||
/**
|
||||
* Buchungskalender-Schalter (Funktionsschluessel 'unterkunft'): UNTERKUNFT_ENABLED = off (Vorgabe) | pilot | on.
|
||||
* Liest config.unterkunft bei jedem Aufruf. Bei off laufen keine Jobs und keine Abfragen des Moduls.
|
||||
*/
|
||||
|
||||
/** 'off' | 'pilot' | 'on'; alles andere (leer, unbekannt) gilt als off. */
|
||||
export function unterkunftModus() {
|
||||
const mode = config.unterkunft.mode
|
||||
return mode === 'pilot' || mode === 'on' ? mode : 'off'
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf dieser Kunde den Buchungskalender nutzen? pilot = nur Adressen aus UNTERKUNFT_PILOT_EMAILS ('*' = alle; eine leere
|
||||
* Liste laesst niemanden durch); on = alle Kunden; off = niemand.
|
||||
*/
|
||||
export function unterkunftFuerKunde(email) {
|
||||
const modus = unterkunftModus()
|
||||
if (modus === 'off') return false
|
||||
if (modus === 'on') return true
|
||||
const erlaubt = config.unterkunft.pilotEmails
|
||||
if (erlaubt.includes('*')) return true
|
||||
const adresse = String(email || '').trim().toLowerCase()
|
||||
return adresse !== '' && erlaubt.includes(adresse)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Buchungskalender: Appwrite-Schema der sechs Collections (Spezifikation 2026-10-07-buchungskalender-design.md, Abschnitt 3).
|
||||
* Reine Daten, absichtlich ohne Import von server/config.js (wie shop/schema.js: test/support/schema.js liest sie vor
|
||||
* useTestEnv()). Die Collection-IDs stehen in config.collections.<Name>; scripts/unterkunft-setup.mjs legt alles an.
|
||||
* Kein Feld heisst data; JSON-Felder sind Text. ukEinstellungen: Dokument-ID = featureId.
|
||||
*
|
||||
* Attribut: { key, type: 'string' | 'integer' | 'boolean' | 'datetime', size (nur Text), required: false }
|
||||
* Index: { key, type: 'key', attributes: [...] }
|
||||
* Datumswerte (dt) sind UTC (toISOString()); Kalendertage (von, bis, datum) sind Text YYYY-MM-DD (Berlin).
|
||||
*/
|
||||
|
||||
const str = (key, size) => ({ key, type: 'string', size, required: false })
|
||||
const int = (key) => ({ key, type: 'integer', required: false })
|
||||
const bool = (key) => ({ key, type: 'boolean', required: false })
|
||||
const dt = (key) => ({ key, type: 'datetime', required: false })
|
||||
const index = (key, ...attributes) => ({ key, type: 'key', attributes })
|
||||
|
||||
export const UNTERKUNFT_COLLECTIONS = {
|
||||
ukUnterkuenfte: {
|
||||
attributes: [
|
||||
str('featureId', 36),
|
||||
str('customerId', 36),
|
||||
str('projectId', 36),
|
||||
str('name', 120),
|
||||
str('beschreibung', 500),
|
||||
int('maxPersonen'),
|
||||
int('mindestNaechte'),
|
||||
str('checkIn', 5),
|
||||
str('checkOut', 5),
|
||||
int('sort'),
|
||||
bool('aktiv'),
|
||||
str('icalImporte', 4000),
|
||||
str('icalExportToken', 64),
|
||||
],
|
||||
indexes: [index('idx_feature', 'featureId')],
|
||||
},
|
||||
ukPreise: {
|
||||
attributes: [
|
||||
str('unterkunftId', 36),
|
||||
str('featureId', 36),
|
||||
str('staffeln', 2000),
|
||||
int('reinigungCent'),
|
||||
int('personenInklusive'),
|
||||
int('aufpreisPersonNachtCent'),
|
||||
int('direktrabattProzent'),
|
||||
str('steuerart', 20),
|
||||
int('ustUebernachtung'),
|
||||
int('ustReinigung'),
|
||||
int('fristTage'),
|
||||
str('stornobedingungen', 5000),
|
||||
],
|
||||
indexes: [index('idx_unterkunft', 'unterkunftId')],
|
||||
},
|
||||
ukSperren: {
|
||||
attributes: [
|
||||
str('unterkunftId', 36),
|
||||
str('featureId', 36),
|
||||
str('von', 10),
|
||||
str('bis', 10),
|
||||
str('quelle', 60),
|
||||
str('notiz', 200),
|
||||
],
|
||||
indexes: [index('idx_unterkunft', 'unterkunftId'), index('idx_unterkunft_quelle', 'unterkunftId', 'quelle')],
|
||||
},
|
||||
ukAnfragen: {
|
||||
attributes: [
|
||||
str('featureId', 36),
|
||||
str('customerId', 36),
|
||||
str('projectId', 36),
|
||||
str('unterkunftId', 36),
|
||||
str('nummer', 12),
|
||||
str('von', 10),
|
||||
str('bis', 10),
|
||||
int('personen'),
|
||||
str('status', 12),
|
||||
str('gast', 3000),
|
||||
str('rechnungsadresse', 1500),
|
||||
str('posten', 8000),
|
||||
str('summen', 1000),
|
||||
str('nachrichtVermieter', 2000),
|
||||
dt('fristAt'),
|
||||
str('tokenHash', 64),
|
||||
str('zahlart', 8),
|
||||
str('bezahltVia', 8),
|
||||
dt('bezahltAt'),
|
||||
str('stripeSessionId', 100),
|
||||
str('stripePaymentIntentId', 100),
|
||||
int('erstattetCent'),
|
||||
str('stornoGrund', 500),
|
||||
str('stornobedingungenText', 5000),
|
||||
str('rechtstexteFassung', 20),
|
||||
str('ipHash', 16),
|
||||
dt('freigegebenAt'),
|
||||
dt('gebuchtAt'),
|
||||
dt('abgelehntAt'),
|
||||
dt('abgelaufenAt'),
|
||||
dt('storniertAt'),
|
||||
str('rechnungId', 36),
|
||||
dt('anonymisiertAt'),
|
||||
],
|
||||
indexes: [
|
||||
index('idx_feature', 'featureId'),
|
||||
index('idx_unterkunft', 'unterkunftId'),
|
||||
index('idx_unterkunft_status', 'unterkunftId', 'status'),
|
||||
index('idx_status', 'status'),
|
||||
],
|
||||
},
|
||||
ukRechnungen: {
|
||||
attributes: [
|
||||
str('featureId', 36),
|
||||
str('anfrageId', 36),
|
||||
str('art', 8),
|
||||
str('nummer', 24),
|
||||
str('datum', 10),
|
||||
int('bruttoCent'),
|
||||
str('inhalt', 20000),
|
||||
],
|
||||
indexes: [index('idx_feature', 'featureId'), index('idx_anfrage', 'anfrageId'), index('idx_feature_nummer', 'featureId', 'nummer')],
|
||||
},
|
||||
ukEinstellungen: {
|
||||
attributes: [
|
||||
str('featureId', 36),
|
||||
str('firma', 150),
|
||||
str('inhaber', 150),
|
||||
str('strasse', 150),
|
||||
str('plz', 10),
|
||||
str('ort', 100),
|
||||
str('email', 200),
|
||||
str('telefon', 50),
|
||||
str('steuernummer', 30),
|
||||
str('ustId', 20),
|
||||
str('iban', 34),
|
||||
str('bic', 11),
|
||||
str('bank', 100),
|
||||
str('rechnungsPraefix', 8),
|
||||
str('rechnungZaehler', 100),
|
||||
str('akzentfarbe', 7),
|
||||
str('stripeAccountId', 40),
|
||||
str('stripeStatus', 4000),
|
||||
bool('onlineZahlung'),
|
||||
dt('rechnungAuftragAt'),
|
||||
str('anfrage', 2000),
|
||||
str('anfrageStatus', 16),
|
||||
dt('anfrageAt'),
|
||||
dt('anfrageEntschiedenAt'),
|
||||
str('anfrageEntschiedenVon', 120),
|
||||
str('anfrageBegruendung', 1000),
|
||||
dt('icalFehlerMailAt'),
|
||||
],
|
||||
indexes: [index('idx_feature', 'featureId')],
|
||||
},
|
||||
}
|
||||
|
||||
/** Je Collection die datetime-Attribute (der Appwrite-Nachbau der Tests normalisiert sie). */
|
||||
export const UNTERKUNFT_DATETIMES = Object.fromEntries(
|
||||
Object.entries(UNTERKUNFT_COLLECTIONS).map(([name, { attributes }]) => [name, attributes.filter((a) => a.type === 'datetime').map((a) => a.key)]),
|
||||
)
|
||||
Reference in new issue
Block a user