- Admin-Tab "Abo-Angebote" und Button "Angebot" in der Kundenliste: Preis, Monate, Gueltigkeit, Hinweis fuer den Kunden, interne Notiz, Vorschau vor dem Anlegen - Je Angebot sofort ein Stripe-Coupon (amount_off, repeating ueber die Angebotsmonate, max. 1 Einloesung); danach automatisch wieder Normalpreis - Kunde ohne Abo: Angebot im Portal, Angebotspreis in der Bestelluebersicht, Coupon im Checkout; Angebot schaltet die Bestellung auch ausserhalb der Pilotliste frei; Bestellung prueft, dass das angezeigte Angebot noch gilt - Laufendes Abo: Coupon sofort am Abo fuer die naechsten Abbuchungen - Testkonten: Testrabatt im Checkout, Angebot per Webhook zusaetzlich am Abo - Angebot in Abo-Metadaten, Vertragsbestaetigung, Admin-Mail und Abo-Karte - Zurueckziehen loescht den Coupon; abgelaufene Angebote nicht mehr bestellbar - Stundenkontingent nach invoice.subtotal: auch voll rabattierte oder per Guthaben bezahlte Monate bringen die Stunde, die Probezeit nicht - setup-abo-collections.mjs: aboOffers, subscriptions.offerId/offerText, Attribut-Abfrage mit Limit (sonst Haenger ab 26 Attributen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.8 KiB
JavaScript
180 lines
7.8 KiB
JavaScript
import 'dotenv/config'
|
||
import path from 'node:path'
|
||
|
||
/** Einzige erlaubte Datenbank (Ticketsystem). Keine separate Portal-DB mehr. */
|
||
export const WOMS_DATABASE_ID = 'woms-database'
|
||
|
||
const DEPRECATED_DATABASE_IDS = new Set([
|
||
'6943bf0e0003291f8c35', // frühere Portal-/Legacy-DB
|
||
])
|
||
|
||
function resolveDatabaseId() {
|
||
const fromEnv = (process.env.APPWRITE_DATABASE_ID || '').trim()
|
||
if (!fromEnv || fromEnv === WOMS_DATABASE_ID) return WOMS_DATABASE_ID
|
||
if (DEPRECATED_DATABASE_IDS.has(fromEnv)) {
|
||
console.warn(
|
||
`[config] Veraltete APPWRITE_DATABASE_ID "${fromEnv}" – es wird nur ${WOMS_DATABASE_ID} verwendet.`
|
||
)
|
||
return WOMS_DATABASE_ID
|
||
}
|
||
console.warn(
|
||
`[config] APPWRITE_DATABASE_ID="${fromEnv}" ignoriert – Portal nutzt ausschließlich ${WOMS_DATABASE_ID}.`
|
||
)
|
||
return WOMS_DATABASE_ID
|
||
}
|
||
|
||
export const config = {
|
||
port: Number(process.env.PORT) || 3000,
|
||
sessionSecret: process.env.SESSION_SECRET || '',
|
||
cookieName: process.env.SESSION_COOKIE_NAME || 'webklar_portal_session',
|
||
allowedCustomerStatuses: (process.env.ALLOWED_CUSTOMER_STATUSES || 'active')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
|
||
appwrite: {
|
||
endpoint: process.env.APPWRITE_ENDPOINT || 'https://ticket.webklar.com/v1',
|
||
projectId: process.env.APPWRITE_PROJECT_ID || '6a1058610003c5a13a05',
|
||
databaseId: resolveDatabaseId(),
|
||
apiKey: process.env.APPWRITE_API_KEY || '',
|
||
},
|
||
|
||
collections: {
|
||
customers: process.env.APPWRITE_COLLECTION_CUSTOMERS || 'customers',
|
||
customerPortalAccess: process.env.APPWRITE_COLLECTION_CUSTOMER_PORTAL_ACCESS || 'customerPortalAccess',
|
||
websiteProjects: process.env.APPWRITE_COLLECTION_WEBSITE_PROJECTS || 'websiteProjects',
|
||
portalFeatures: process.env.APPWRITE_COLLECTION_PORTAL_FEATURES || 'portalFeatures',
|
||
workorders: process.env.APPWRITE_COLLECTION_WORKORDERS || 'workorders',
|
||
worksheets: process.env.APPWRITE_COLLECTION_WORKSHEETS || 'worksheets',
|
||
employees: process.env.APPWRITE_COLLECTION_EMPLOYEES || 'employees',
|
||
portalMessages: process.env.APPWRITE_COLLECTION_PORTAL_MESSAGES || 'portalMessages',
|
||
subscriptions: process.env.APPWRITE_COLLECTION_SUBSCRIPTIONS || 'subscriptions',
|
||
hourCredits: process.env.APPWRITE_COLLECTION_HOUR_CREDITS || 'hourCredits',
|
||
takeoverCredits: process.env.APPWRITE_COLLECTION_TAKEOVER_CREDITS || 'takeoverCredits',
|
||
aboOffers: process.env.APPWRITE_COLLECTION_ABO_OFFERS || 'aboOffers',
|
||
},
|
||
|
||
publicBaseUrl: (process.env.PORTAL_PUBLIC_URL || 'https://project.webklar.com').replace(/\/$/, ''),
|
||
|
||
// Wartungs-Abo (Stripe Billing). Preis/Produkt legt der Server per lookup_key selbst an.
|
||
stripe: {
|
||
secretKey: process.env.STRIPE_SECRET_KEY || '',
|
||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||
priceLookupKey: process.env.STRIPE_ABO_PRICE_LOOKUP_KEY || 'webklar_wartung_monatlich_5000',
|
||
priceAmountCents: 5000,
|
||
productName: 'WEBklar Wartungs-Abo',
|
||
productDescription:
|
||
'Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland (Sicherheitsupdates, SSL, tägliche Datensicherung, Überwachung der Erreichbarkeit), 1 Domain inklusive Verwaltung, 1 E-Mail-Postfach (5 GB) und 1 Stunde Arbeitszeit pro Monat (12 Monate nutzbar). Mindestlaufzeit 12 Monate, danach monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.',
|
||
statementDescriptor: 'WEBKLAR WARTUNG',
|
||
productImage: 'https://project.webklar.com/img/wartungs-abo-produkt.png',
|
||
// Kleinunternehmer: Pflichthinweis auf jeder Stripe-Rechnung
|
||
invoiceFooter:
|
||
process.env.STRIPE_INVOICE_FOOTER ||
|
||
'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet (Kleinunternehmerregelung). WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler',
|
||
},
|
||
|
||
abo: {
|
||
// Bei jeder Textaenderung an Bedingungen oder Bestellseite hochziehen (wird mit der Zustimmung gespeichert)
|
||
termsVersion: '2026-09-25-v3',
|
||
// Mindestlaufzeit ab Leistungsbeginn; Kuendigung zum Ende bis 1 Monat vorher, danach monatlich
|
||
minTermMonths: 12,
|
||
minutesPerMonth: 60,
|
||
creditValidityMonths: 12,
|
||
usageRoundingMinutes: 15,
|
||
withdrawalDays: 14,
|
||
// "Beginn nach Ablauf der Widerrufsfrist": 15 Tage Probezeit ohne Leistung und ohne Abbuchung
|
||
lateStartTrialDays: 15,
|
||
// Pilotbetrieb: nur diese Kunden-E-Mails duerfen bestellen (leer = alle)
|
||
pilotEmails: (process.env.ABO_PILOT_EMAILS || '')
|
||
.split(',')
|
||
.map((s) => s.trim().toLowerCase())
|
||
.filter(Boolean),
|
||
// Testkonten zahlen nur den Stripe-Mindestbetrag (0,50 €) ueber einen Rabatt auf den echten Preis
|
||
testEmails: (process.env.ABO_TEST_EMAILS || '')
|
||
.split(',')
|
||
.map((s) => s.trim().toLowerCase())
|
||
.filter(Boolean),
|
||
testCouponId: 'WEBKLAR-TEST-050',
|
||
},
|
||
|
||
// Individuelle Abo-Angebote (z. B. erste 2 Monate je 25 €): je Angebot ein einmal einloesbarer Stripe-Coupon
|
||
offers: {
|
||
maxMonths: 12,
|
||
defaultValidityDays: 30,
|
||
maxValidityDays: 366,
|
||
},
|
||
|
||
// Uebernahmegutschrift: beim bisherigen Anbieter bereits bezahlte Zeit wird mit den Abo-Gebuehren verrechnet
|
||
takeover: {
|
||
// Bei jeder Textaenderung an den Bedingungen der Gutschrift hochziehen (wird mit dem Antrag gespeichert)
|
||
conditionsVersion: '2026-09-25',
|
||
// Nachweise liegen ausserhalb von public/ (gitignored, ueberlebt Deploys wie preview-data/)
|
||
uploadDir: process.env.TAKEOVER_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'takeover-credits'),
|
||
maxFiles: 3,
|
||
maxFileBytes: 8 * 1024 * 1024,
|
||
maxTotalBytes: 10 * 1024 * 1024,
|
||
// Plausibilitaetsgrenze fuer eine einzelne Gutschrift (Tippfehler abfangen)
|
||
maxAmountCents: 100000,
|
||
maxOpenPerCustomer: 3,
|
||
// Nachweise abgelehnter Antraege werden nach dieser Frist geloescht (Datenschutzerklaerung Abschnitt 7)
|
||
rejectedRetentionDays: 183,
|
||
},
|
||
|
||
smtp: {
|
||
host: process.env.SMTP_HOST || '',
|
||
port: Number(process.env.SMTP_PORT) || 587,
|
||
secure: (process.env.SMTP_SECURE || 'false') === 'true',
|
||
user: process.env.SMTP_USERNAME || '',
|
||
pass: process.env.SMTP_PASSWORD || '',
|
||
from: process.env.SMTP_FROM || process.env.SMTP_USERNAME || '',
|
||
},
|
||
|
||
chatNotify: {
|
||
emails: (process.env.CHAT_NOTIFY_EMAILS || 'kenso@webklar.com,andrej@webklar.com')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
throttleMin: Number(process.env.CHAT_NOTIFY_THROTTLE_MIN) || 15,
|
||
},
|
||
|
||
gitea: {
|
||
webhookToken: process.env.GITEA_WEBHOOK_TOKEN || '',
|
||
baseUrl: process.env.GITEA_BASE_URL || 'https://git.webklar.com',
|
||
apiToken: process.env.GITEA_API_TOKEN || '',
|
||
templateOwner: process.env.GITEA_TEMPLATE_OWNER || 'knso',
|
||
templateRepo: process.env.GITEA_TEMPLATE_REPO || 'webklar-preview-template',
|
||
defaultOwner: process.env.GITEA_DEFAULT_OWNER || 'knso',
|
||
},
|
||
|
||
cors: {
|
||
allowedOrigins: (process.env.ADMIN_CORS_ORIGINS || 'https://ticket.webklar.com,http://localhost:5173')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean),
|
||
},
|
||
|
||
preview: {
|
||
baseHost: process.env.PREVIEW_BASE_HOST || 'project.webklar.com',
|
||
deployRoot: process.env.PREVIEW_DEPLOY_ROOT || '',
|
||
traefikDynamicDir: process.env.TRAEFIK_DYNAMIC_DIR || '',
|
||
sessionSecret:
|
||
process.env.PREVIEW_SESSION_SECRET || process.env.SESSION_SECRET || '',
|
||
cookieDomain: process.env.PREVIEW_COOKIE_DOMAIN || '.project.webklar.com',
|
||
sessionMaxAge: Number(process.env.PREVIEW_SESSION_MAX_AGE || 60 * 60 * 24 * 7),
|
||
},
|
||
|
||
invoiceNinja: {
|
||
url: (process.env.INVOICE_NINJA_URL || 'https://invoice.webklar.com').replace(/\/$/, ''),
|
||
apiToken: process.env.INVOICE_NINJA_API_TOKEN || '',
|
||
},
|
||
}
|
||
|
||
export function assertServerConfig() {
|
||
if (!config.sessionSecret || config.sessionSecret.length < 32) {
|
||
console.warn('[config] SESSION_SECRET fehlt oder ist zu kurz (min. 32 Zeichen).')
|
||
}
|
||
if (!config.appwrite.apiKey) {
|
||
console.warn('[config] APPWRITE_API_KEY fehlt – DB-Zugriff und Webhook schlagen fehl.')
|
||
}
|
||
}
|