From 71b2543e95cc12b249e4e29d0f57073fc43dc665 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 19:58:05 +0000 Subject: [PATCH] Kundenansicht neu P7: Wortlaut-Entwurf und Nachweistexte der Bestellseiten Co-Authored-By: Claude Sonnet 5.5 --- server/services/bestelltexte.js | 161 ++++++++++++++++++++++++++++++++ test/knBestelltexte.test.js | 106 +++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 server/services/bestelltexte.js create mode 100644 test/knBestelltexte.test.js diff --git a/server/services/bestelltexte.js b/server/services/bestelltexte.js new file mode 100644 index 0000000..79cc9d2 --- /dev/null +++ b/server/services/bestelltexte.js @@ -0,0 +1,161 @@ +/** + * Sätze der Bestellseiten (Abo bestellen, Wechsel, Zusatzspeicher-Plus, Antrag auf Übernahmegutschrift) in Fassung 6. + * + * ENTWURF, Stand 16.10.2026: Der Wortlaut ist nicht freigegeben (E20). Stellt der Inhaber ihn frei, wird nur WORTLAUT_STATUS auf + * 'freigegeben' umgestellt; Code und Oberfläche lesen den Status aus dieser Datei. + * + * Quelle von Linkzeile und Häkchen ist WORTLAUT_V6 in products.js (P8). Diese Datei bleibt ohne Imports, damit Server, Tests und + * Werkzeug sie ohne Umgebung laden; sie hält die beiden Sätze als Kopie, und test/knBestelltexte.test.js sowie + * test/knVerdrahtung.test.js binden die Gleichheit mit WORTLAUT_V6 und mitFassung. Ändert sich der Wortlaut dort, werden diese + * Tests rot und die Kopie hier zieht nach. Die übrigen Sätze (Sperrzeile, Warnzeile, Gutschrift-Satz, Kundenart-Zeile, Fehlertexte) + * stehen nur hier und in docs/rechtliches/kundenansicht-entscheidungen.md. + * + * Markierungen: In den Vorlagen stehen Link-Texte in eckigen Klammern. klartext() entfernt die Klammern; die Adressen dazu stehen + * als urls in derselben Reihenfolge. Die Oberfläche setzt daraus Links (nie HTML aus dem Text), der Nachweis speichert den Klartext. + * + * Fassungsangabe: Alle Sätze nennen die Fassung als Versionszeichenfolge (z. B. 2026-10-16-v6), so wie mitFassung sie einsetzt. + */ + +/** 'entwurf' | 'freigegeben'; stellt nur der Inhaber (P8) um. */ +export const WORTLAUT_STATUS = 'entwurf' + +/** Text des Knopfs, wörtlich (rechtlich fest, Spec 4). */ +export const KNOPF = 'Zahlungspflichtig bestellen' + +/** Knopf des Antrags auf Übernahmegutschrift. */ +export const KNOPF_ANTRAG = 'Antrag absenden' + +export const WECHSEL_GESPERRT = 'Der Wechsel gilt sofort. Ohne dein Häkchen können wir ihn nicht ausführen.' +export const FEHLER_NACHWEIS = 'Das hat gerade nicht geklappt. Bitte versuche es gleich noch einmal.' +export const FEHLER_BEDINGUNGEN = 'Die Bedingungen wurden aktualisiert. Bitte lade die Seite neu.' +export const FEHLER_UNVOLLSTAENDIG = 'Die Bestellung ist unvollständig. Bitte lade die Seite neu.' + +const DATENSCHUTZ = '/datenschutz.html' + +// Kopie aus WORTLAUT_V6 (products.js); {fassung} füllt bestelltexteFuerClient mit der Versionszeichenfolge +const LINKZEILE = + 'Es gelten die [Vertragsbedingungen] mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung {fassung}). Alles kommt danach auch als PDF per Mail. [Datenschutz]' +const HAKEN_ABO = 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.' +const HAKEN_WECHSEL = 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.' + +const WARNUNG_SPEICHER = + 'Wichtig: Passen die gespeicherten E-Mails nach der Verkleinerung nicht mehr in den verbleibenden Speicher, nehmen die Postfächer keine neuen E-Mails mehr an. Bitte räume vorher auf. Mehr dazu in den [Regeln zum Zusatzspeicher].' +const SATZ_GUTSCHRIFT = + 'Mit dem Absenden bestätigst du, dass deine Angaben stimmen und die Rechnung auf dich bzw. dein Unternehmen ausgestellt ist. Es gelten die [Bedingungen der Übernahmegutschrift]. Angaben und Nachweise verwendet WEBklar zur Prüfung und Abwicklung des Antrags ([Datenschutz]).' +const KUNDENART_BETRIEB = 'Du bestellst als Betrieb' +const KUNDENART_PRIVAT = 'Du bestellst privat' +const KUNDENART_KORREKTUR = 'Stimmt nicht? [Schreib uns im Chat]' + +const ARTEN = ['abo', 'wechsel', 'speicher'] + +/** 'Es gilt [A] und [B].' wird zu 'Es gilt A und B.' */ +export function klartext(vorlage) { + if (typeof vorlage !== 'string') throw new TypeError('Vorlage muss ein Text sein') + return vorlage.replace(/\[([^\]]*)\]/g, '$1') +} + +function fassungPruefen(fassung) { + if (typeof fassung !== 'string' || fassung.trim() === '') throw new Error('Fassung fehlt oder ist leer') + return fassung +} + +const mitFassung = (vorlage, fassung) => vorlage.split('{fassung}').join(fassungPruefen(fassung)) + +/** + * Nutzlast für GET /api/abo (neue Ansicht): die Sätze der vier Seiten mit den Adressen ihrer Link-Texte. + * @param {{ fassung: string, termsUrl: string }} p fassung: Versionszeichenfolge (config.abo.termsVersionNeu); termsUrl: Adresse der Bedingungen + */ +export function bestelltexteFuerClient({ fassung, termsUrl }) { + fassungPruefen(fassung) + if (typeof termsUrl !== 'string' || termsUrl === '') throw new Error('Adresse der Bedingungen fehlt') + const linkzeile = { text: mitFassung(LINKZEILE, fassung), urls: [termsUrl, DATENSCHUTZ] } + return { + status: WORTLAUT_STATUS, + fassung, + knopf: KNOPF, + abo: { linkzeile, haken: HAKEN_ABO }, + wechsel: { linkzeile: { text: linkzeile.text, urls: [...linkzeile.urls] }, haken: HAKEN_WECHSEL, gesperrt: WECHSEL_GESPERRT }, + speicher: { + linkzeile: { text: linkzeile.text, urls: [`${termsUrl}#zusatzspeicher`, DATENSCHUTZ] }, + warnung: { text: WARNUNG_SPEICHER, urls: [`${termsUrl}#zusatzspeicher`] }, + }, + gutschrift: { satz: { text: SATZ_GUTSCHRIFT, urls: ['#gutschrift?bedingungen=1', `${DATENSCHUTZ}#uebernahmegutschrift`] }, knopf: KNOPF_ANTRAG }, + kundenart: { unternehmen: KUNDENART_BETRIEB, privat: KUNDENART_PRIVAT, korrektur: { text: KUNDENART_KORREKTUR, urls: ['#chat'] } }, + } +} + +function euro(cents) { + if (!Number.isInteger(cents) || cents < 0) throw new TypeError('Betrag muss eine ganze Zahl in Cent sein') + return `${(cents / 100).toFixed(2).replace('.', ',')} €` +} + +function datum(iso) { + const ms = Date.parse(iso) + if (!Number.isFinite(ms)) throw new TypeError('Datum ungültig') + return new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(ms)) +} + +function text(wert, name) { + if (typeof wert !== 'string' || wert.trim() === '') throw new TypeError(`${name} fehlt`) + return wert +} + +function zahl(wert, name) { + if (!Number.isInteger(wert) || wert < 1) throw new TypeError(`${name} muss eine positive ganze Zahl sein`) + return wert +} + +const UST = 'gemäß § 19 UStG ohne Umsatzsteuer' + +/** + * Die Zeile „bestellung“ des Nachweises: eine Zusammenfassung aus Werten, die der Server selbst kennt oder geprüft hat. + * @param {'abo'|'wechsel'|'speicher'} art + * @param {object} fakten abo: { produkt, monatCents, mindestMonate, beginn: 'sofort'|'nach_frist', beginnAm?, angebot? }; + * wechsel: { heuteCents, monatCents, mindestMonate }; speicher: { units, gb, monatCents } + */ +export function bestellZeile(art, fakten) { + const f = fakten ?? {} + if (art === 'abo') { + const nachFrist = f.beginn === 'nach_frist' + if (!nachFrist && f.beginn !== 'sofort') throw new TypeError('Beginn muss sofort oder nach_frist sein') + const angebot = f.angebot ? `individuelles Angebot: ${text(f.angebot, 'Angebot')}, ` : '' + const beginn = nachFrist ? `Beginn nach Ablauf der Widerrufsfrist am ${datum(f.beginnAm)}` : 'Beginn sofort' + return `${text(f.produkt, 'Produkt')}: ${euro(f.monatCents)} pro Monat (Gesamtpreis, ${UST}), ${angebot}${beginn}, Mindestlaufzeit ${zahl(f.mindestMonate, 'Mindestlaufzeit')} Monate ab Leistungsbeginn` + } + if (art === 'wechsel') { + return `Wechsel ins Wartungs-Abo, Beginn sofort. Heute fällig: ${euro(f.heuteCents)} (${UST}), danach ${euro(f.monatCents)} pro Monat zuzüglich gebuchtem Zusatzspeicher, Mindestlaufzeit ${zahl(f.mindestMonate, 'Mindestlaufzeit')} Monate ab dem Wechsel` + } + if (art === 'speicher') { + const n = zahl(f.units, 'Pakete') + return `Zusatzspeicher: ${n} ${n === 1 ? 'Paket' : 'Pakete'} (+${zahl(f.gb, 'Speicher')} GB), ${euro(f.monatCents)} pro Monat (${UST}), monatlich kündbar` + } + throw new TypeError(`Unbekannte Art: ${String(art)}`) +} + +/** + * Anzeigereihenfolge des Nachweises als Klartext: bestellung (nur bei nichtleerer Zeile), linkzeile, beginn (nur wenn gezeigt und + * gesetzt), knopf, kundenart. Gutschrift: satz, knopf. + * @param {'abo'|'wechsel'|'speicher'|'gutschrift'} art + * @param {{ kundenart?: string, fassung?: string, bestellung?: string, beginnGehakt?: boolean }} p + * kundenart: 'unternehmen' oder (alles andere) 'privat', vom Server aus dem Kundendokument; beginnGehakt: das Häkchen war gezeigt und gesetzt + * @returns {Array<{ id: string, text: string, haken: boolean }>} + */ +export function nachweisTexte(art, { kundenart, fassung, bestellung = '', beginnGehakt = false } = {}) { + if (art === 'gutschrift') { + return [ + { id: 'satz', text: klartext(SATZ_GUTSCHRIFT), haken: false }, + { id: 'knopf', text: KNOPF_ANTRAG, haken: false }, + ] + } + if (!ARTEN.includes(art)) throw new TypeError(`Unbekannte Art: ${String(art)}`) + const betrieb = kundenart === 'unternehmen' + const zeilen = [] + if (typeof bestellung === 'string' && bestellung !== '') zeilen.push({ id: 'bestellung', text: bestellung, haken: false }) + zeilen.push({ id: 'linkzeile', text: klartext(mitFassung(LINKZEILE, fassung)), haken: false }) + if ((art === 'abo' || art === 'wechsel') && !betrieb && beginnGehakt === true) { + zeilen.push({ id: 'beginn', text: art === 'abo' ? HAKEN_ABO : HAKEN_WECHSEL, haken: true }) + } + zeilen.push({ id: 'knopf', text: KNOPF, haken: false }) + zeilen.push({ id: 'kundenart', text: `${betrieb ? KUNDENART_BETRIEB : KUNDENART_PRIVAT} · ${klartext(KUNDENART_KORREKTUR)}`, haken: false }) + return zeilen +} diff --git a/test/knBestelltexte.test.js b/test/knBestelltexte.test.js new file mode 100644 index 0000000..26e7acf --- /dev/null +++ b/test/knBestelltexte.test.js @@ -0,0 +1,106 @@ +import { test, after } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { startAppwriteMock } from './support/appwriteMock.js' +import { startFakeStripe } from './support/fakeStripe.js' +import { useTestEnv } from './support/testEnv.js' + +// Sätze der Bestellseiten (Fassung 6, Entwurf): Linkzeile und Häkchen sind Kopien von WORTLAUT_V6 und hier gebunden. +const aw = await startAppwriteMock() +const st = await startFakeStripe() +after(() => Promise.all([aw.close(), st.close()])) +useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) +const { WORTLAUT_STATUS, KNOPF, WECHSEL_GESPERRT, FEHLER_NACHWEIS, FEHLER_BEDINGUNGEN, FEHLER_UNVOLLSTAENDIG, klartext, bestelltexteFuerClient, bestellZeile, nachweisTexte } = await import('../server/services/bestelltexte.js') +const { config } = await import('../server/config.js') +const { WORTLAUT_V6, mitFassung, termsDocFor } = await import('../server/services/products.js') +const FASSUNG = config.abo.termsVersionNeu +const URL_V6 = termsDocFor(FASSUNG).url +const BESTELLEN = mitFassung(WORTLAUT_V6.linkzeile.bestellen, FASSUNG) // der Satz aus P8, Fassungsangabe = Versionszeichenfolge +const T = bestelltexteFuerClient({ fassung: FASSUNG, termsUrl: URL_V6 }) + +test('Status, Knopf und feste Fehlertexte', () => { + assert.deepEqual([WORTLAUT_STATUS, KNOPF], ['entwurf', 'Zahlungspflichtig bestellen']) + assert.equal(WECHSEL_GESPERRT, 'Der Wechsel gilt sofort. Ohne dein Häkchen können wir ihn nicht ausführen.') + assert.equal(FEHLER_NACHWEIS, 'Das hat gerade nicht geklappt. Bitte versuche es gleich noch einmal.') + assert.equal(FEHLER_BEDINGUNGEN, 'Die Bedingungen wurden aktualisiert. Bitte lade die Seite neu.') + assert.equal(FEHLER_UNVOLLSTAENDIG, 'Die Bestellung ist unvollständig. Bitte lade die Seite neu.') +}) + +test('klartext und Fassungsangabe', () => { + assert.equal(klartext('Es gilt [A] und [B].'), 'Es gilt A und B.') + for (const leer of ['', undefined, null, 6]) assert.throws(() => bestelltexteFuerClient({ fassung: leer, termsUrl: URL_V6 }), /Fassung/) +}) + +test('Nutzlast: Linkzeile und Häkchen sind der Wortlaut aus P8 (WORTLAUT_V6), dazu Sperrzeile, Warnzeile, Satz, Kundenart-Zeile (Entwurf)', () => { + assert.deepEqual([T.status, T.fassung, T.knopf], ['entwurf', FASSUNG, 'Zahlungspflichtig bestellen']) + assert.equal(klartext(T.abo.linkzeile.text), `${BESTELLEN} Datenschutz`) + assert.deepEqual(T.abo.linkzeile.text.match(/\[[^\]]+\]/g), ['[Vertragsbedingungen]', '[Datenschutz]']) + assert.deepEqual(T.abo.linkzeile.urls, [URL_V6, '/datenschutz.html']) + assert.equal(T.abo.haken, WORTLAUT_V6.beginn.abo) + assert.deepEqual(T.wechsel.linkzeile, T.abo.linkzeile) + assert.equal(T.wechsel.haken, WORTLAUT_V6.beginn.wechsel) + assert.equal(T.wechsel.gesperrt, WECHSEL_GESPERRT) + assert.equal(klartext(T.speicher.linkzeile.text), `${BESTELLEN} Datenschutz`) + assert.deepEqual(T.speicher.linkzeile.urls, [`${URL_V6}#zusatzspeicher`, '/datenschutz.html']) + assert.equal(T.speicher.warnung.text, 'Wichtig: Passen die gespeicherten E-Mails nach der Verkleinerung nicht mehr in den verbleibenden Speicher, nehmen die Postfächer keine neuen E-Mails mehr an. Bitte räume vorher auf. Mehr dazu in den [Regeln zum Zusatzspeicher].') + assert.deepEqual(T.speicher.warnung.urls, [`${URL_V6}#zusatzspeicher`]) + assert.equal(T.gutschrift.satz.text, 'Mit dem Absenden bestätigst du, dass deine Angaben stimmen und die Rechnung auf dich bzw. dein Unternehmen ausgestellt ist. Es gelten die [Bedingungen der Übernahmegutschrift]. Angaben und Nachweise verwendet WEBklar zur Prüfung und Abwicklung des Antrags ([Datenschutz]).') + assert.deepEqual(T.gutschrift.satz.urls, ['#gutschrift?bedingungen=1', '/datenschutz.html#uebernahmegutschrift']) + assert.equal(T.gutschrift.knopf, 'Antrag absenden') + assert.deepEqual(T.kundenart, { unternehmen: 'Du bestellst als Betrieb', privat: 'Du bestellst privat', korrektur: { text: 'Stimmt nicht? [Schreib uns im Chat]', urls: ['#chat'] } }) + // jede Markierung hat genau eine Adresse + for (const s of [T.abo.linkzeile, T.wechsel.linkzeile, T.speicher.linkzeile, T.speicher.warnung, T.gutschrift.satz, T.kundenart.korrektur]) { + assert.equal((s.text.match(/\[[^\]]+\]/g) ?? []).length, s.urls.length, s.text) + } +}) + +test('Nutzlast ist unabhängig vom Aufrufer: eigene Kopien, nichts geteilt', () => { + const a = bestelltexteFuerClient({ fassung: FASSUNG, termsUrl: URL_V6 }) + a.abo.linkzeile.urls.push('x') + a.wechsel.linkzeile.urls.push('y') + assert.deepEqual(bestelltexteFuerClient({ fassung: FASSUNG, termsUrl: URL_V6 }).wechsel.linkzeile.urls, [URL_V6, '/datenschutz.html']) + assert.throws(() => bestelltexteFuerClient({ fassung: FASSUNG, termsUrl: '' }), /Adresse/) +}) + +test('bestellZeile: feste Zeilen aus geprüften Werten', () => { + assert.equal(bestellZeile('abo', { produkt: 'Wartungs-Abo', monatCents: 5000, mindestMonate: 12, beginn: 'sofort' }), 'Wartungs-Abo: 50,00 € pro Monat (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), Beginn sofort, Mindestlaufzeit 12 Monate ab Leistungsbeginn') + assert.equal(bestellZeile('abo', { produkt: 'Hosting-Abo', monatCents: 2000, mindestMonate: 12, beginn: 'nach_frist', beginnAm: '2026-10-30T05:00:00.000Z', angebot: 'Erste 2 Monate je 10,00 €' }), 'Hosting-Abo: 20,00 € pro Monat (Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer), individuelles Angebot: Erste 2 Monate je 10,00 €, Beginn nach Ablauf der Widerrufsfrist am 30.10.2026, Mindestlaufzeit 12 Monate ab Leistungsbeginn') + assert.equal(bestellZeile('wechsel', { heuteCents: 4100, monatCents: 5000, mindestMonate: 12 }), 'Wechsel ins Wartungs-Abo, Beginn sofort. Heute fällig: 41,00 € (gemäß § 19 UStG ohne Umsatzsteuer), danach 50,00 € pro Monat zuzüglich gebuchtem Zusatzspeicher, Mindestlaufzeit 12 Monate ab dem Wechsel') + assert.equal(bestellZeile('speicher', { units: 2, gb: 10, monatCents: 200 }), 'Zusatzspeicher: 2 Pakete (+10 GB), 2,00 € pro Monat (gemäß § 19 UStG ohne Umsatzsteuer), monatlich kündbar') + assert.equal(bestellZeile('speicher', { units: 1, gb: 5, monatCents: 100 }), 'Zusatzspeicher: 1 Paket (+5 GB), 1,00 € pro Monat (gemäß § 19 UStG ohne Umsatzsteuer), monatlich kündbar') + assert.throws(() => bestellZeile('gutschrift', {})) + // ungeprüfte Werte erzeugen keine Zeile + assert.throws(() => bestellZeile('abo', { produkt: 'Wartungs-Abo', monatCents: 50.5, mindestMonate: 12, beginn: 'sofort' }), TypeError) + assert.throws(() => bestellZeile('abo', { produkt: 'Wartungs-Abo', monatCents: 5000, mindestMonate: 12, beginn: 'irgendwann' }), TypeError) + assert.throws(() => bestellZeile('abo', { produkt: 'Wartungs-Abo', monatCents: 5000, mindestMonate: 12, beginn: 'nach_frist', beginnAm: 'morgen' }), TypeError) + assert.throws(() => bestellZeile('speicher', { units: 0, gb: 5, monatCents: 100 }), TypeError) +}) + +test('nachweisTexte: Reihenfolge, Häkchen nur wenn gezeigt und gesetzt, Klartext', () => { + const fassung = FASSUNG + const betrieb = nachweisTexte('abo', { kundenart: 'unternehmen', fassung, bestellung: 'Zeile' }) + assert.deepEqual(betrieb.map((t) => t.id), ['bestellung', 'linkzeile', 'knopf', 'kundenart']) + assert.equal(betrieb[1].text, `${BESTELLEN} Datenschutz`) + assert.deepEqual(betrieb[3], { id: 'kundenart', text: 'Du bestellst als Betrieb · Stimmt nicht? Schreib uns im Chat', haken: false }) + const privat = nachweisTexte('abo', { kundenart: 'privat', fassung, bestellung: 'Zeile', beginnGehakt: true }) + assert.deepEqual(privat.map((t) => [t.id, t.haken]), [['bestellung', false], ['linkzeile', false], ['beginn', true], ['knopf', false], ['kundenart', false]]) + assert.equal(privat[2].text, WORTLAUT_V6.beginn.abo) + assert.ok(!nachweisTexte('abo', { kundenart: 'privat', fassung, bestellung: 'Zeile' }).some((t) => t.id === 'beginn')) + assert.ok(!nachweisTexte('wechsel', { kundenart: 'unternehmen', fassung, bestellung: 'Zeile', beginnGehakt: true }).some((t) => t.id === 'beginn')) + assert.equal(nachweisTexte('wechsel', { kundenart: 'privat', fassung, bestellung: 'Z', beginnGehakt: true })[2].text, WORTLAUT_V6.beginn.wechsel) + assert.deepEqual(nachweisTexte('speicher', { kundenart: 'privat', fassung, bestellung: 'Z', beginnGehakt: true }).map((t) => t.id), ['bestellung', 'linkzeile', 'knopf', 'kundenart']) + assert.deepEqual(nachweisTexte('gutschrift', {}).map((t) => [t.id, t.text]), [['satz', klartext(T.gutschrift.satz.text)], ['knopf', 'Antrag absenden']]) + assert.throws(() => nachweisTexte('unbekannt', {})) + // leere und unbekannte Kundenart gelten als privat (E4); ohne Fassung entsteht kein Nachweis + assert.equal(nachweisTexte('abo', { kundenart: '', fassung, bestellung: 'Z' }).at(-1).text, 'Du bestellst privat · Stimmt nicht? Schreib uns im Chat') + assert.equal(nachweisTexte('abo', { kundenart: 'kaputt', fassung, bestellung: 'Z', beginnGehakt: true }).some((t) => t.id === 'beginn'), true) + assert.throws(() => nachweisTexte('abo', { kundenart: 'privat', fassung: '', bestellung: 'Z' }), /Fassung/) + assert.equal(nachweisTexte('abo', { kundenart: 'privat', fassung }).some((t) => t.id === 'bestellung'), false, 'ohne Zeile keine Zeile') +}) + +test('Quelle: keine Imports, du-Form, als Entwurf gekennzeichnet', () => { + const q = readFileSync(new URL('../server/services/bestelltexte.js', import.meta.url), 'utf8') + assert.doesNotMatch(q, /^import /m) + assert.doesNotMatch(q, /\b(Sie|Ihr|Ihre|Ihnen)\b/) + assert.match(q, /ENTWURF/) +})