// Durchlauf über alle neuen Endpunkte von „Aufträge und Rechnungen“ (Strang Z, Task Z1.3). // Echter Server-Code (Router, Dienste, pdfkit); Nachbauten für Appwrite, Stripe und SMTP, nichts verlässt den Rechner. // Aufruf: cd && free -m && timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/auftraegeAblauf.test.js // Jeder Test hat eigene Kunden und Projekte; die AB-Nummern laufen über alle Tests (D15 prüft sie am Ende). // Der Vertrag entscheidet (Übersicht V1–V8, Spec). Was der Vertrag offen lässt, steht als „Annahme“ im Kommentar. import { test, after, beforeEach } from 'node:test' import assert from 'node:assert/strict' import crypto from 'node:crypto' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { inspect } from 'node:util' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' import { schemaKeys } from './support/schema.js' import { mailSink } from './support/mailSink.js' import { AUFTRAEGE_COLLECTIONS, AUFTRAEGE_ZUSATZ_ATTRIBUTE } from '../server/services/auftraege/schema.js' // --- Umgebung ------------------------------------------------------------------------------------------------ // Ablage der AB-PDFs in einem Wegwerf-Ordner und Schalter an (Vorgabe wäre off): config.js liest beides beim ersten Import. const ABLAGE = fs.mkdtempSync(path.join(os.tmpdir(), 'auftraege-ablauf-')) process.env.AUFTRAEGE_UPLOAD_DIR = ABLAGE process.env.AUFTRAEGE_ENABLED = 'on' // Alles, was dieser Prozess ins Protokoll schreibt, wird gesammelt: D16 prüft, dass kein Token und keine Client-Adresse darin steht. const PROTOKOLL = [] for (const stufe of ['log', 'info', 'warn', 'error']) { const original = console[stufe].bind(console) console[stufe] = (...teile) => { PROTOKOLL.push(teile.map((teil) => (typeof teil === 'string' ? teil : inspect(teil, { depth: 5 }))).join(' ')) original(...teile) } } const felder = (liste) => liste.map((attribut) => attribut.key) const datumsfelder = (liste) => liste.filter((attribut) => attribut.type === 'datetime').map((attribut) => attribut.key) const aw = await startAppwriteMock({ schema: { auftraege: felder(AUFTRAEGE_COLLECTIONS.auftraege.attributes), projektRechnungen: felder(AUFTRAEGE_COLLECTIONS.projektRechnungen.attributes), financeSettings: [...new Set([...schemaKeys('financeSettings'), ...felder(AUFTRAEGE_ZUSATZ_ATTRIBUTE.financeSettings)])], }, datetimes: { auftraege: datumsfelder(AUFTRAEGE_COLLECTIONS.auftraege.attributes), projektRechnungen: datumsfelder(AUFTRAEGE_COLLECTIONS.projektRechnungen.attributes), }, defaultLimit: 25, // wie Appwrite: ohne limit() kommen nur 25 Dokumente zurück (die Termine-Tests prüfen es ebenso) }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { config } = await import('../server/config.js') const { startTestApp, send, sessionCookie } = await import('./support/testApp.js') const { kundenansichtCookie } = await import('./support/fixtures.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js') const { invalidateFinanceCache, resetInvoiceCreateGuardForTests } = await import('../server/services/finance/stripeFinance.js') const { customerStripeInvoice } = await import('../server/services/customerInvoices.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { addDays, berlinDate, formatDe } = await import('../server/services/finance/dates.js') const { withdrawalDeadline } = await import('../server/services/deadlines.js') const { AGB_FASSUNG, SCHALTER_AUS_MELDUNG } = await import('../server/services/auftraege/konstanten.js') const { formatNummer } = await import('../server/services/auftraege/nummern.js') const { hashToken } = await import('../server/services/auftraege/token.js') const { FRUEHBEGINN_TEXT } = await import('../server/services/auftraege/texte.js') const { default: adminProjektkontenRoutes } = await import('../server/routes/admin/projektkonten.js') const { default: adminAuftraegeRoutes } = await import('../server/routes/admin/auftraege.js') const { default: adminFinanzenRoutes } = await import('../server/routes/admin/finanzen.js') const { default: publicAuftragRoutes } = await import('../server/routes/auftrag.js') const { default: kundeAuftraegeRoutes } = await import('../server/routes/auftraegeKunde.js') const { default: aboRoutes, resetPublicRateLimit } = await import('../server/routes/abo.js') const { default: teamRoutes } = await import('../server/routes/team/index.js') // Eingebunden wie in server/index.js (V5 „Einbindung“): Wächter der Kundenansicht zuerst, spezifische Admin-Pfade vor dem Finanzen-Router const app = await startTestApp({ '/': kundenansichtGuard, '/api/admin/finanzen/projektkonten': adminProjektkontenRoutes, '/api/admin/finanzen/auftraege': adminAuftraegeRoutes, '/api/admin/finanzen': adminFinanzenRoutes, '/api/auftrag': publicAuftragRoutes, '/api/auftraege': kundeAuftraegeRoutes, '/api/abo': aboRoutes, '/api/team': teamRoutes, }) const mails = mailSink() setMailTransportForTests(mails) after(async () => { setMailTransportForTests(null) await Promise.all([app.close(), aw.close(), st.close()]) // nur der selbst angelegte Wegwerf-Ordner, nie ein Pfad von außen if (ABLAGE.startsWith(`${os.tmpdir()}${path.sep}auftraege-ablauf-`)) fs.rmSync(ABLAGE, { recursive: true, force: true }) }) beforeEach(() => { resetInvoiceCreateGuardForTests() // 30-Sekunden-Schutz gegen gleiche Rechnungen invalidateFinanceCache() // Stripe-Zwischenspeicher (60 s) resetPublicRateLimit() // Widerrufsseite: 10 Erklärungen je Stunde und Adresse }) // --- Hilfen -------------------------------------------------------------------------------------------------- const ADMIN = { Authorization: 'Bearer jwt-admin' } const STAFF = { Authorization: 'Bearer jwt-staff' } // Mitarbeiter ohne Admin-Label const KV = 'a'.repeat(32) // Kennung der Kundenansicht (Kopfzeile X-Kundenansicht) const UNBEKANNT = 'Dieser Link ist ungültig oder abgelaufen.' const HEUTE = berlinDate() const JAHR = Number(HEUTE.slice(0, 4)) const AB = () => config.collections.auftraege const PR = () => config.collections.projektRechnungen const ip = (n) => ({ 'x-forwarded-for': `203.0.113.${n}` }) // je Test eine eigene Client-Adresse (Ratenbegrenzung je Adresse) const sha256 = (daten) => crypto.createHash('sha256').update(daten).digest('hex') const pause = (ms) => new Promise((fertig) => setTimeout(fertig, ms)) const kundeCookie = (id) => sessionCookie({ customerId: id, appwriteUserId: `u_${id}`, name: `Kontakt ${id}`, email: `${id}@kunde.example` }) const admin = (methode, pfad, body, headers = {}) => send(methode, `/api/admin/finanzen${pfad}`, { body, headers: { ...ADMIN, ...headers } }) function erwarte(res, status, was, meldung) { assert.equal(res.status, status, `${was}: erwartet ${status}, bekam ${res.status} ${JSON.stringify(res.body)}`) if (meldung instanceof RegExp) assert.match(String(res.body?.error ?? ''), meldung, `${was}: Meldung`) else if (typeof meldung === 'string') assert.equal(res.body?.error, meldung, `${was}: Meldung`) return res.body } // Annahme (nicht im Vertrag): Erfolg heißt 200 oder 201 function erfolg(res, was) { assert.ok(res.status === 200 || res.status === 201, `${was}: erwartet 200 oder 201, bekam ${res.status} ${JSON.stringify(res.body)}`) return res.body } async function holePdf(pfad, { headers = {}, cookie } = {}) { const res = await fetch(app.url + pfad, { headers: { ...(cookie ? { cookie } : {}), ...headers } }) return { status: res.status, typ: res.headers.get('content-type') || '', bytes: Buffer.from(await res.arrayBuffer()) } } const istPdf = (bytes) => bytes.subarray(0, 5).toString('latin1') === '%PDF-' && bytes.length > 1500 && bytes.toString('latin1', Math.max(0, bytes.length - 64)).includes('%%EOF') const seiten = (bytes) => (bytes.toString('latin1').match(/\/Type \/Page(?![s\w])/g) || []).length const pdfDateien = () => (fs.existsSync(ABLAGE) ? fs.readdirSync(ABLAGE, { recursive: true }).filter((name) => String(name).endsWith('.pdf')) : []) const adressen = (feld) => (feld || []).map((eintrag) => eintrag.address) const findeMail = (vorher, muster) => mails.sent.slice(vorher).find((mail) => muster.test(mail.subject)) async function warteBis(pruefung, { ms = 5000, was = 'Bedingung' } = {}) { const ende = Date.now() + ms for (;;) { const wert = await pruefung() if (wert) return wert if (Date.now() > ende) assert.fail(`Zeitüberschreitung beim Warten auf: ${was}`) await pause(25) } } const GESEHEN = new Set() // alle Token aus Mails und Fehlversuchen: D16 sucht sie in Dokumenten und im Protokoll const TOKEN_IM_LINK = /\/auftrag\.html#t=([A-Za-z0-9_-]{43})(?![A-Za-z0-9_-])/ // V6: K31 verlinkt auftrag.html#t= (Fragment: landet in keinem Server- oder Proxy-Protokoll) function tokenAus(mail) { const treffer = String(mail.html).match(TOKEN_IM_LINK) || String(mail.text).match(TOKEN_IM_LINK) assert.ok(treffer, `kein Bestätigungslink in der Mail "${mail.subject}"`) GESEHEN.add(treffer[1]) return treffer[1] } // V6: K31 geht OHNE BCC an den Kunden (die Mail trägt den Bestätigungslink); das Team bekommt eine eigene Kopie ohne Link (Block teamkopie), // auch bei Erinnerungen. k31Mails trennt beide. const kundenAdresse = (kundenId) => `${kundenId}@kunde.example` function k31Mails(vorher, kundenId) { const alle = mails.sent.slice(vorher).filter((mail) => /Auftragsbestätigung AB-\d{4}-\d{4}/.test(mail.subject)) const anKunde = alle.filter((mail) => adressen(mail.to).includes(kundenAdresse(kundenId))) return { anKunde, teamkopien: alle.filter((mail) => !anKunde.includes(mail)) } } function pruefeTeamkopie(kundenMail, token, teamkopien, was) { assert.deepEqual(adressen(kundenMail.bcc), [], `${was}: K31 an den Kunden hat keine BCC-Kopie (V6)`) assert.ok(teamkopien.length >= 1, `${was}: das Team bekommt eine Kopie der K31`) const empfaenger = teamkopien.flatMap((mail) => adressen(mail.to)) for (const adresse of config.chatNotify.emails) assert.ok(empfaenger.includes(adresse), `${was}: Teamkopie an ${adresse}`) for (const kopie of teamkopien) { const inhalt = `${kopie.html}\n${kopie.text}` assert.equal(inhalt.includes(token), false, `${was}: die Teamkopie enthält den Token`) assert.equal(/auftrag\.html[?#]/.test(inhalt), false, `${was}: die Teamkopie enthält einen Bestätigungslink`) assert.match(kopie.text, /Kopie für das Team/, `${was}: Hinweis „Kopie für das Team“ statt des Knopfes`) } } const stripeAufrufe = (ab, methode, pfad) => st.calls.slice(ab).filter((aufruf) => aufruf.method === methode && (pfad instanceof RegExp ? pfad.test(aufruf.path) : aufruf.path === pfad)) const zeilen = (rechnungId) => st.invoices.get(rechnungId).lines.data function bezahlen(rechnungId) { const roh = st.invoices.get(rechnungId) assert.ok(roh, `Rechnung ${rechnungId} fehlt im Stripe-Nachbau`) st.invoices.set(rechnungId, { ...roh, status: 'paid', paid: true, amount_paid: roh.total, amount_remaining: 0, status_transitions: { ...roh.status_transitions, paid_at: Math.floor(Date.now() / 1000) }, }) invalidateFinanceCache() } async function rechnung(body, was) { return erfolg(await admin('POST', '/rechnungen', { daysUntilDue: 14, senden: false, ...body }), was).rechnung } async function konto(kundenId, projektId) { const body = erwarte(await admin('GET', `/projektkonten?kunde=${kundenId}`), 200, `Projektkonten von ${kundenId}`) const eintrag = body.konten.find((k) => k.projektId === projektId) assert.ok(eintrag, `Projekt ${projektId} fehlt in den Projektkonten von ${kundenId}`) return { body, konto: eintrag } } const AB_BASIS = { titel: 'Neue Website', art: 'festpreis', projektId: '', positionen: [{ beschreibung: 'Konzept und Gestaltung', menge: 1, einzelpreisCents: 50000 }], stundensatzCents: null, schaetzungMinuten: null, zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' }, zahlungszielTage: 14, auftragVom: addDays(HEUTE, -1), auftragWeg: 'telefon', startAm: addDays(HEUTE, 7), fertigBis: addDays(HEUTE, 40), zeitquelle: [], } async function entwurf(kundenId, ueberschreibe = {}) { const auftrag = erfolg(await admin('POST', '/auftraege', { ...AB_BASIS, kundenId, ...ueberschreibe }), `AB-Entwurf für ${kundenId}`).auftrag assert.equal(auftrag.status, 'entwurf') assert.equal(auftrag.nummer, '', 'Entwürfe haben keine Nummer') return auftrag } const lies = async (id) => erwarte(await admin('GET', `/auftraege/${id}`), 200, `AB ${id} lesen`).auftrag async function sende(id, was = 'AB senden') { const vorher = mails.sent.length const body = erwarte(await admin('POST', `/auftraege/${id}/senden`), 200, was) assert.equal(body.mailFehler, false, `${was}: Mail nicht zugestellt`) const { anKunde, teamkopien } = k31Mails(vorher, body.auftrag.kundenId) assert.equal(anKunde.length, 1, `${was}: genau eine K31 an den Kunden (${mails.sent.slice(vorher).map((m) => `${m.subject} an ${adressen(m.to)}`).join(' | ')})`) const token = tokenAus(anKunde[0]) pruefeTeamkopie(anKunde[0], token, teamkopien, was) return { auftrag: body.auftrag, mail: anKunde[0], token } } const ansicht = (token, n) => send('GET', `/api/auftrag/${token}`, { headers: ip(n) }) const bestaetigen = (token, n, body = { bestaetigt: true, fruehbeginn: false }, extra = {}) => send('POST', `/api/auftrag/${token}/bestaetigen`, { body, headers: { ...ip(n), ...(extra.headers || {}) }, cookie: extra.cookie }) const vergebeneNummern = () => [...aw.docs(AB()).values()].filter((d) => d.nummer).map((d) => d.nummer).sort() // --- Testdaten ----------------------------------------------------------------------------------------------- const ADRESSE = { street: 'Hauptstraße 1', postalCode: '10115', city: 'Berlin', country: 'Deutschland' } const kundendoc = (id, ueberschreibe = {}) => ({ $id: id, appwriteUserId: `u_${id}`, name: `Kontakt ${id}`, email: `${id}@kunde.example`, code: `K-${id}`, customerStatus: 'active', status: 'active', portalAccessEnabled: true, kundenart: 'unternehmen', ...ADRESSE, ...ueberschreibe, }) const KUNDEN = { kfirma: kundendoc('kfirma', { companyName: 'Beispiel GmbH', name: 'Anna Beispiel' }), kprivat: kundendoc('kprivat', { kundenart: 'privat', name: 'Petra Privat' }), kfremd: kundendoc('kfremd', { companyName: 'Fremd AG', name: 'Frank Fremd' }), kluecke: kundendoc('kluecke', { kundenart: '', name: 'Lena Lücke' }), } for (const doc of Object.values(KUNDEN)) aw.seed('customers', doc) const setzeKunde = (id, patch) => aw.seed('customers', { ...KUNDEN[id], ...patch }) function legeProjektAn(id, kundenId, ticketId, woid, name) { aw.seed('workorders', { $id: ticketId, woid, customerId: kundenId, topic: name, status: 'Open', type: 'Webpage' }) aw.seed('websiteProjects', { $id: id, customerId: kundenId, ticketId, projectName: name, projectType: 'website', status: 'active' }) } legeProjektAn('pfest', 'kfirma', 'tkfest', '100201', 'Website Beispiel') legeProjektAn('paufw', 'kfirma', 'tkaufw', '100202', 'Online-Shop Beispiel') legeProjektAn('pab', 'kfirma', 'tkab', '100203', 'Relaunch Größe & Maße') legeProjektAn('pstorno', 'kfirma', 'tkstorno', '100204', 'Landingpage Beispiel') legeProjektAn('pzuordnen', 'kfirma', 'tkzuordnen', '100205', 'Projekt zum Zuordnen') legeProjektAn('pfremd', 'kfremd', 'tkfremd', '100299', 'Fremdes Projekt') const blatt = (id, ticketId, ueberschreibe) => ({ $id: id, workorderId: ticketId, woid: ticketId === 'tkaufw' ? '100202' : '100299', serviceType: 'Remote', employeeName: 'Justin Klein', totalTime: 60, startDate: '06.10.2026', endDate: '06.10.2026', details: 'Arbeit', isComment: false, countsToPlan: false, ...ueberschreibe, }) const ANDREJ = [{ employeeId: 'andrej', name: 'Andrej Stevanovski', task: 'Farbkonzept' }] // Zeit des Projekts „Online-Shop“: 06.10. = 60 + 75 min, 07.10. = 50 → 60 min, 08.10. = 40 + 30 Unterstützung → 75 min; zusammen 270 min = 4,5 Std. aw.seed('worksheets', blatt('wsA', 'tkaufw', { totalTime: 60, details: 'Startseite' })) aw.seed('worksheets', blatt('wsB', 'tkaufw', { totalTime: 75, details: 'Kontaktformular' })) aw.seed('worksheets', blatt('wsC', 'tkaufw', { totalTime: 50, startDate: '07.10.2026', endDate: '07.10.2026', details: 'Impressum und Datenschutz' })) aw.seed('worksheets', blatt('wsD', 'tkaufw', { totalTime: 40, startDate: '08.10.2026', endDate: '08.10.2026', details: 'Farbkonzept', supporters: JSON.stringify(ANDREJ) })) // nie abrechenbar: Abo-Zeit, Kommentar, 0 Minuten; dazu ein Blatt eines anderen Kunden aw.seed('worksheets', blatt('wsAbo', 'tkaufw', { totalTime: 90, countsToPlan: true, details: 'Wartung im Abo' })) aw.seed('worksheets', blatt('wsKomm', 'tkaufw', { totalTime: 30, isComment: true, details: 'Interner Hinweis' })) aw.seed('worksheets', blatt('wsNull', 'tkaufw', { totalTime: 0, details: 'Telefonat ohne Zeit' })) aw.seed('worksheets', blatt('wsFremd', 'tkfremd', { totalTime: 60, details: 'Arbeit beim anderen Kunden' })) // --- D0: Einbindung, Seiten und Zugriffsschutz --------------------------------------------------------------- test('D0 Einbindung: server/index.js hängt die neuen Router ein (Admin-Pfade vor dem Finanzen-Router), das Team-Gerüst hängt hinter requireStaffJwt', () => { const index = fs.readFileSync(new URL('../server/index.js', import.meta.url), 'utf8') const stelle = (pfad, was) => { const treffer = new RegExp(`app\\.use\\(\\s*['"]${pfad}['"]\\s*,\\s*\\w+\\s*\\)`).exec(index) assert.ok(treffer, `server/index.js: ${was} (${pfad}) fehlt`) return treffer.index } const finanzen = stelle('/api/admin/finanzen', 'Finanzen-Router') assert.ok(stelle('/api/admin/finanzen/projektkonten', 'Projektkonten') < finanzen, 'Projektkonten vor dem Finanzen-Router') assert.ok(stelle('/api/admin/finanzen/auftraege', 'Aufträge (Admin)') < finanzen, 'Aufträge vor dem Finanzen-Router') stelle('/api/auftrag', 'öffentliche Token-Routen') stelle('/api/auftraege', 'Kundenrouten') const team = fs.readFileSync(new URL('../server/routes/team/index.js', import.meta.url), 'utf8') const jwt = team.indexOf('router.use(requireStaffJwt)') assert.ok(jwt >= 0 && team.indexOf("router.use('/arbeitsblaetter'") > jwt, "Team: router.use('/arbeitsblaetter', …) steht hinter requireStaffJwt") }) test('D0 Seiten: die Bestätigungsseite ist noindex, die Widerrufsseite kennt den Projektauftrag', () => { const auftrag = fs.readFileSync(new URL('../public/auftrag.html', import.meta.url), 'utf8') assert.match(auftrag, /]*(?:name=["']robots["'][^>]*noindex|noindex[^>]*name=["']robots["'])/i, 'auftrag.html trägt noindex') const widerruf = fs.readFileSync(new URL('../public/widerrufen.html', import.meta.url), 'utf8') assert.match(widerruf, /value=["']projekt["']/, 'Vertragsart mit order=projekt') assert.match(widerruf, /Projektauftrag/, 'Beschriftung „Projektauftrag (Auftragsbestätigung)“') }) test('D0 Zugriff: ohne Anmeldung 401, Mitarbeiter ohne Admin-Label 403, Kunden-Cookie ersetzt kein JWT, unbekannte Token einheitlich 404', async () => { const adminRouten = [ ['GET', '/projektkonten?kunde=kfirma'], ['GET', '/projektkonten/pfest'], ['GET', '/auftraege?kunde=kfirma'], ['POST', '/auftraege'], ['GET', '/auftraege/x'], ['PATCH', '/auftraege/x'], ['DELETE', '/auftraege/x'], ['GET', '/auftraege/x/pdf'], ['POST', '/auftraege/x/senden'], ['POST', '/auftraege/x/erinnern'], ['POST', '/auftraege/x/neue-fassung'], ['POST', '/auftraege/x/stornieren'], ['POST', '/auftraege/x/projekt'], ['POST', '/rechnungen/vorschau'], ] for (const [methode, pfad] of adminRouten) { const body = methode === 'GET' || methode === 'DELETE' ? undefined : {} const was = `${methode} ${pfad}` erwarte(await send(methode, `/api/admin/finanzen${pfad}`, { body }), 401, `${was} ohne JWT`) erwarte(await send(methode, `/api/admin/finanzen${pfad}`, { body, cookie: kundeCookie('kfirma') }), 401, `${was} mit Kunden-Cookie`) erwarte(await send(methode, `/api/admin/finanzen${pfad}`, { body, headers: STAFF }), 403, `${was} als Mitarbeiter ohne Admin-Label`) } const abgerechnet = '/api/team/arbeitsblaetter/abgerechnet?ticketId=tkaufw' erwarte(await send('GET', abgerechnet), 401, 'Team-Route ohne JWT') erwarte(await send('GET', abgerechnet, { cookie: kundeCookie('kfirma') }), 401, 'Team-Route mit Kunden-Cookie') erwarte(await send('GET', abgerechnet, { headers: { Authorization: 'Bearer jwt-kunde' } }), 403, 'Team-Route mit Kundenkonto ohne Label') assert.deepEqual(Object.keys(erwarte(await send('GET', abgerechnet, { headers: STAFF }), 200, 'Team-Route als Mitarbeiter')), ['abgerechnet']) for (const [methode, pfad] of [['GET', '/api/auftraege'], ['GET', '/api/auftraege/x/pdf'], ['POST', '/api/auftraege/x/bestaetigen']]) { erwarte(await send(methode, pfad, { body: methode === 'POST' ? { bestaetigt: true } : undefined }), 401, `${methode} ${pfad} ohne Sitzung`) } for (const token of ['x', 'kurz', 'A'.repeat(43), 'A'.repeat(44), `${'A'.repeat(42)}%2F`]) { erwarte(await ansicht(token, 1), 404, `Ansicht mit Token ${token}`, UNBEKANNT) erwarte(await send('GET', `/api/auftrag/${token}/pdf`, { headers: ip(1) }), 404, `PDF mit Token ${token}`, UNBEKANNT) erwarte(await bestaetigen(token, 2, { bestaetigt: true }), 404, `Bestätigen mit Token ${token}`, UNBEKANNT) } }) // --- D1: Einstellungen --------------------------------------------------------------------------------------- test('D1 Einstellungen: die Steuernummer wird geprüft und gespeichert (sie steht später auf dem AB-PDF)', async () => { assert.equal(erfolg(await admin('GET', '/einstellungen'), 'Einstellungen lesen').einstellungen.steuernummer, '') // V4: erlaubt sind Buchstaben, Ziffern, Leerzeichen, / - und . (auch eine USt-IdNr. „DE…“), höchstens 40 Zeichen for (const falsch of ['12/345/67890