Aufträge: Konfiguration und Pilot-Schalter AUFTRAEGE_ENABLED

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-07 14:04:48 +00:00
1 parent 0ab3cbbaa3
commit ddffc7a92d
3 files changed
+135

No files matched your search

+14
View File
@@ -32,6 +32,7 @@ function nonNegativeNumber(raw, fallback) {
const termineEnabled = (process.env.TERMINE_ENABLED || '').trim().toLowerCase()
const shopEnabled = (process.env.SHOP_ENABLED || '').trim().toLowerCase()
const zeitenEnabled = (process.env.ZEITEN_ENABLED || '').trim().toLowerCase()
const auftraegeEnabled = (process.env.AUFTRAEGE_ENABLED || '').trim().toLowerCase()
/** Komma-Liste aus einer Umgebungsvariablen: getrimmt, klein geschrieben, ohne Leereintraege; leer ergibt den Vorgabewert. */
function commaList(raw, fallback) {
@@ -634,6 +635,9 @@ export const config = {
// Oeffnungszeiten live (Einstellungen, Erinnerungsmerker; scripts/zeiten-setup.mjs)
oeffnungszeiten: process.env.APPWRITE_COLLECTION_OEFFNUNGSZEITEN || 'oeffnungszeiten',
zeitenErinnerungen: process.env.APPWRITE_COLLECTION_ZEITEN_ERINNERUNGEN || 'zeitenErinnerungen',
// Aufträge und Rechnungen (Auftragsbestätigungen, Projektrechnungen; scripts/auftraege-setup.mjs)
auftraege: process.env.APPWRITE_COLLECTION_AUFTRAEGE || 'auftraege',
projektRechnungen: process.env.APPWRITE_COLLECTION_PROJEKT_RECHNUNGEN || 'projektRechnungen',
},
publicBaseUrl: (process.env.PORTAL_PUBLIC_URL || 'https://project.webklar.com').replace(/\/$/, ''),
@@ -927,6 +931,16 @@ export const config = {
keinNeubau: commaList(process.env.ZEITEN_KEIN_NEUBAU, ['emmas-schatzkiste']),
},
// Aufträge und Rechnungen: Schalter nur für die AB-Funktionen und die Rechnungsarten anzahlung/schluss; wird bei
// jedem Aufruf gelesen, siehe server/services/auftraege/schalter.js
auftraege: {
// off (Vorgabe) | pilot (nur AUFTRAEGE_PILOT_EMAILS, '*' = alle) | on
mode: ['pilot', 'on'].includes(auftraegeEnabled) ? auftraegeEnabled : 'off',
pilotEmails: commaList(process.env.AUFTRAEGE_PILOT_EMAILS, []),
// Gesendete AB-PDFs: <uploadDir>/<kundenId>/<auftragId>.pdf
uploadDir: process.env.AUFTRAEGE_UPLOAD_DIR || path.join(process.cwd(), 'upload-data', 'auftraege'),
},
invoiceNinja: {
url: (process.env.INVOICE_NINJA_URL || 'https://invoice.webklar.com').replace(/\/$/, ''),
apiToken: process.env.INVOICE_NINJA_API_TOKEN || '',
+21
View File
@@ -0,0 +1,21 @@
import { config } from '../../config.js'
/**
* Pilot-Schalter der Aufträge (AUFTRAEGE_ENABLED = off (Vorgabe) | pilot | on). Betrifft nur die AB-Funktionen und
* die Rechnungsarten anzahlung/schluss; Projektkonto und die Arten frei, festbetrag, aufwand laufen immer.
* Liest config.auftraege bei jedem Aufruf.
*/
/**
* Darf dieser Kunde Auftragsbestätigungen nutzen? `kunde`: Kundendokument oder { email }. on = alle; pilot = Liste
* enthält '*' oder die (klein geschriebene, getrimmte) E-Mail-Adresse steht in AUFTRAEGE_PILOT_EMAILS (eine leere
* Liste lässt niemanden durch); alles andere, auch ein unbekannter Modus, gilt als off.
*/
export function auftraegeAktiv(kunde) {
const { mode, pilotEmails } = config.auftraege
if (mode === 'on') return true
if (mode !== 'pilot') return false
if (pilotEmails.includes('*')) return true
const adresse = String(kunde?.email || '').trim().toLowerCase()
return adresse !== '' && pilotEmails.includes(adresse)
}
+100
View File
@@ -0,0 +1,100 @@
// test/auftraegeSchalter.test.js
import { test, beforeEach, afterEach } from 'node:test'
import assert from 'node:assert/strict'
import path from 'node:path'
import { config } from '../server/config.js'
import { auftraegeAktiv } from '../server/services/auftraege/schalter.js'
const ENV = ['AUFTRAEGE_ENABLED', 'AUFTRAEGE_PILOT_EMAILS', 'AUFTRAEGE_UPLOAD_DIR', 'APPWRITE_COLLECTION_AUFTRAEGE', 'APPWRITE_COLLECTION_PROJEKT_RECHNUNGEN']
let original
beforeEach(() => {
original = { ...config.auftraege }
})
afterEach(() => {
Object.assign(config.auftraege, original)
})
/** Frische config.js-Instanz mit genau diesen Umgebungsvariablen (alle anderen entfernt). */
let freshCounter = 0
async function freshConfig(env = {}) {
const saved = Object.fromEntries(ENV.map((key) => [key, process.env[key]]))
for (const key of ENV) delete process.env[key]
Object.assign(process.env, env)
try {
freshCounter += 1
return (await import(`../server/config.js?auftraege-test=${freshCounter}`)).config
} finally {
for (const key of ENV) {
if (saved[key] === undefined) delete process.env[key]
else process.env[key] = saved[key]
}
}
}
test('Vorgabe: aus, keine Pilotadressen, Ablage unter upload-data/auftraege, Collection-IDs wie die Namen', async () => {
const fresh = await freshConfig()
assert.deepEqual(fresh.auftraege, { mode: 'off', pilotEmails: [], uploadDir: path.join(process.cwd(), 'upload-data', 'auftraege') })
assert.equal(fresh.collections.auftraege, 'auftraege')
assert.equal(fresh.collections.projektRechnungen, 'projektRechnungen')
})
test('AUFTRAEGE_ENABLED: leer und Unbekanntes sind off, " ON " ist on, " Pilot " ist pilot mit bereinigter Liste', async () => {
for (const value of ['', ' ', 'banane', 'true', '1']) {
assert.equal((await freshConfig({ AUFTRAEGE_ENABLED: value })).auftraege.mode, 'off', JSON.stringify(value))
}
assert.equal((await freshConfig({ AUFTRAEGE_ENABLED: ' ON ' })).auftraege.mode, 'on')
const fresh = await freshConfig({ AUFTRAEGE_ENABLED: ' Pilot ', AUFTRAEGE_PILOT_EMAILS: ' Kenso.Gri@Gmail.com, ,justin@webklar.com ' })
assert.equal(fresh.auftraege.mode, 'pilot')
assert.deepEqual(fresh.auftraege.pilotEmails, ['kenso.gri@gmail.com', 'justin@webklar.com'])
})
test('Ablage und Collection-IDs lassen sich per Umgebung ändern', async () => {
const fresh = await freshConfig({
AUFTRAEGE_UPLOAD_DIR: '/tmp/ab-test',
APPWRITE_COLLECTION_AUFTRAEGE: 'abX',
APPWRITE_COLLECTION_PROJEKT_RECHNUNGEN: 'prX',
})
assert.equal(fresh.auftraege.uploadDir, '/tmp/ab-test')
assert.deepEqual([fresh.collections.auftraege, fresh.collections.projektRechnungen], ['abX', 'prX'])
})
test('off sperrt alle, auch mit Stern in der Liste; ein unbekannter Modus gilt als off', () => {
config.auftraege.mode = 'off'
config.auftraege.pilotEmails = ['*', 'kenso.gri@gmail.com']
assert.equal(auftraegeAktiv({ email: 'kenso.gri@gmail.com' }), false)
assert.equal(auftraegeAktiv({ email: 'jemand@example.com' }), false)
for (const value of [undefined, null, '', 'banane', 'PILOT']) {
config.auftraege.mode = value
assert.equal(auftraegeAktiv({ email: 'kenso.gri@gmail.com' }), false, String(value))
}
})
test('pilot: nur Adressen der Liste, ohne Beachtung von Groß- und Kleinschreibung und Leerraum', () => {
config.auftraege.mode = 'pilot'
config.auftraege.pilotEmails = ['a@b.de']
assert.equal(auftraegeAktiv({ email: 'A@b.de' }), true)
assert.equal(auftraegeAktiv({ email: ' a@b.de ' }), true)
assert.equal(auftraegeAktiv({ email: 'x@y.de' }), false)
assert.equal(auftraegeAktiv({ email: 'a@b.de.evil.de' }), false)
for (const kunde of [undefined, null, {}, { email: '' }, { email: ' ' }]) assert.equal(auftraegeAktiv(kunde), false)
})
test('pilot mit Stern lässt alle durch, auch ohne Adresse; eine leere Liste niemanden', () => {
config.auftraege.mode = 'pilot'
config.auftraege.pilotEmails = ['*']
assert.equal(auftraegeAktiv({ email: 'x@y.de' }), true)
assert.equal(auftraegeAktiv(null), true)
config.auftraege.pilotEmails = []
assert.equal(auftraegeAktiv({ email: 'a@b.de' }), false)
})
test('on gilt für alle; ein Kundendokument zählt wie { email }', () => {
config.auftraege.mode = 'on'
config.auftraege.pilotEmails = []
assert.equal(auftraegeAktiv({ email: 'x@y.de' }), true)
assert.equal(auftraegeAktiv(undefined), true)
config.auftraege.mode = 'pilot'
config.auftraege.pilotEmails = ['k@example.invalid']
assert.equal(auftraegeAktiv({ $id: 'c1', email: 'k@example.invalid', name: 'Kunde Eins' }), true)
})