Files
Webklar-Kundenbereich/test/kundenansichtKurzinfo.test.js
T
ClaudeandClaude Sonnet 5.5 53a191219e Kundenansicht neu (Meine, Live-Detail): GET /api/funktionen liefert je Projekt und Funktion die Kurzinfo
In der neuen Ansicht trägt jede Funktion unter projects[].features.<key> zusätzlich kurzinfo (Text oder null).
Nur aktive Funktionen bekommen einen Text: Terminplaner "3 Termine heute · nächster 10:30", Webshop "2 neue
Bestellungen", News-Pinnwand "4 Beiträge sichtbar", Buchungskalender "1 offene Anfrage" bzw. "Nächste Anreise
24.10.", Öffnungszeiten live der Status der Website (ohne eigene Abfrage). Berliner Zeit, je Funktion höchstens
zwei schmale Abfragen (limit, select, Index-Felder), alles gleichzeitig; jede Funktion einzeln begrenzt (1,5 s)
und fehlertolerant: Fehler oder Zeitüberschreitung ergibt null. Ohne neue Ansicht bleibt die Antwort wie sie
war und es wird nichts gelesen. Die Analyse hat keine Kurzinfo (Zahlen kommen aus dem Auswertungsdienst).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:09:25 +00:00

360 lines
21 KiB
JavaScript

import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
import { SHOP_DATETIMES } from '../server/services/shop/schema.js'
import { UNTERKUNFT_DATETIMES } from '../server/services/unterkunft/schema.js'
// GET /api/funktionen in der neuen Ansicht: je Projekt und Funktion `kurzinfo` (Live-Detail der Zeile in "Meine"). Nur aktive Funktionen
// bekommen einen Text, jede Einzelabfrage ist begrenzt und fehlertolerant (Fehler oder Zeitueberschreitung ergibt null), Berliner Zeit.
// Ohne neue Ansicht bleibt die Antwort wie sie war und nichts davon wird gelesen.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
featureConsents: schemaKeys('featureConsents'),
shopConfig: schemaKeys('shopConfig'),
ukEinstellungen: schemaKeys('ukEinstellungen'),
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
termineBookings: schemaKeys('termineBookings'),
shopOrders: schemaKeys('shopOrders'),
newsPosts: schemaKeys('newsPosts'),
ukAnfragen: schemaKeys('ukAnfragen'),
},
datetimes: {
...ZEITEN_DATETIMES,
termineBookings: TERMINE_DATETIMES.termineBookings,
shopOrders: SHOP_DATETIMES.shopOrders,
ukAnfragen: UNTERKUNFT_DATETIMES.ukAnfragen,
newsPosts: ['visibleFrom', 'visibleUntil', 'takenDownAt', 'createdAt', 'updatedAt'],
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
featureConsents: ['acceptedAt'],
},
defaultLimit: 25,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, get } = await import('./support/testApp.js')
const { customerCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
const { einschaltenDeps } = await import('../server/services/zeiten/einschalten.js')
const { kurzinfoDeps } = await import('../server/services/kundenansicht/kurzinfo.js')
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
after(() => Promise.all([app.close(), aw.close(), st.close()]))
after(() => setMailTransportForTests(null))
const JETZT = new Date('2026-10-17T08:00:00.000Z') // Samstag 10:00 Uhr in Berlin (Sommerzeit)
const c1 = customerCookie('c1')
const vorher = {
neu: { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] },
termine: config.termine.mode,
shop: { mode: config.shop.mode, pilotEmails: [...config.shop.pilotEmails] },
unterkunft: { mode: config.unterkunft.mode, pilotEmails: [...config.unterkunft.pilotEmails] },
zeiten: { mode: config.zeiten.mode, pilotEmails: [...config.zeiten.pilotEmails] },
analyse: config.analyse.enabled,
}
after(() => {
Object.assign(config.kundenansichtNeu, vorher.neu)
config.termine.mode = vorher.termine
Object.assign(config.shop, vorher.shop)
Object.assign(config.unterkunft, vorher.unterkunft)
Object.assign(config.zeiten, vorher.zeiten)
config.analyse.enabled = vorher.analyse
})
const SAMMLUNGEN = ['subscriptions', 'portalFeatures', 'featureConsents', 'oeffnungszeiten', 'workorders', 'websiteProjects', 'contractEvents', 'shopConfig', 'ukEinstellungen', 'termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
const projekt = (id, over = {}) => aw.seed('websiteProjects', { $id: id, customerId: 'c1', projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static', ...over })
const fid = (p, key) => `f_${p}_${key}`
const funktion = (p, key, state = 'active', over = {}) =>
aw.seed('portalFeatures', { $id: fid(p, key), customerId: 'c1', projectId: p, featureKey: key, siteKey: `${key}${p}`.padEnd(16, 'x').slice(0, 16), state, enabled: state === 'active', ...over })
const NAMEN = ['news', 'termine', 'shop', 'unterkunft', 'zeiten']
const alleAktiv = (p) => NAMEN.forEach((k) => funktion(p, k))
const inStd = (h) => new Date(JETZT.getTime() + h * 3600000).toISOString()
let zaehler = 0
const nr = () => `d${(zaehler += 1)}`
const buchung = (p, over) => aw.seed('termineBookings', { $id: nr(), featureId: fid(p, 'termine'), customerId: 'c1', projectId: p, status: 'bestaetigt', quelle: 'online', endsAt: inStd(1), blockEndsAt: inStd(1), ...over })
const bestellung = (p, status, over = {}) => aw.seed('shopOrders', { $id: nr(), shopId: fid(p, 'shop'), customerId: 'c1', status, bestellnummer: nr(), summeCent: 1000, erstelltAt: inStd(-5), ...over })
const zettel = (p, over = {}) => aw.seed('newsPosts', { $id: nr(), customerId: 'c1', projectId: p, type: 'note', title: 'Hallo', text: 'Text', paper: 'sun', duration: 'open', visibleFrom: inStd(-48), visibleUntil: null, takenDownAt: null, createdAt: inStd(-48), updatedAt: inStd(-48), ...over })
const anfrage = (p, status, von, bis, over = {}) => aw.seed('ukAnfragen', { $id: nr(), featureId: fid(p, 'unterkunft'), customerId: 'c1', projectId: p, unterkunftId: 'u1', nummer: nr(), von, bis, status, personen: 2, ...over })
function plan(woche) {
const tage = {}
for (let d = 1; d <= 7; d += 1) tage[d] = woche[d] ? { modus: 'zeiten', fenster: [woche[d]] } : { modus: 'zu', fenster: [] }
return { id: 'p1', kurz: 'standard', name: 'Standard', haupt: true, tage }
}
const zeitenDaten = (woche) => ({ land: 'RP', plaene: [plan(woche)], feiertage: {}, ausnahmen: [], hinweise: [], kontakt: { an: false, telefon: '', adresse: '', whatsapp: null } })
const zeitenDok = (p, daten) => aw.seed('oeffnungszeiten', { $id: fid(p, 'zeiten'), customerId: 'c1', projectId: p, featureId: fid(p, 'zeiten'), version: 1, teamVersion: 0, daten: JSON.stringify(daten) })
const holen = async (cookie = c1) => (await get('/api/funktionen', { cookie }))
const info = (b, p, key) => b.projects.find((x) => x.projectId === p).features[key].kurzinfo
const abfragen = (sammlung) => aw.listCalls(sammlung)
beforeEach(async () => {
await leere(...SAMMLUNGEN)
resetZeitenCacheForTests()
kurzinfoDeps.limitMs = 1500
zaehler = 0
seedFixtures(aw)
aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc }))
projekt('p1')
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
config.termine.mode = 'on'
config.shop.mode = 'on'
config.unterkunft.mode = 'on'
config.zeiten.mode = 'pilot'
config.zeiten.pilotEmails = ['*']
config.analyse.enabled = false
einschaltenDeps.jetzt = () => JETZT
setMailTransportForTests(mailSink())
mock.method(console, 'error', () => {})
mock.method(console, 'warn', () => {})
mock.method(console, 'log', () => {})
aw.resetListCalls()
})
afterEach(() => {
mock.restoreAll()
einschaltenDeps.jetzt = () => new Date()
})
/* ------------------------------------------------------------------------------------------ Terminplaner --- */
test('Terminplaner: Termine von heute (Berliner Tag), nächster nach jetzt; Sperren, Abgesagte, andere Tage und andere Funktionen zählen nicht', async () => {
funktion('p1', 'termine'); projekt('p2'); funktion('p2', 'termine')
buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T06:00:00.000Z' }) // 08:00 Berlin, schon vorbei, zählt
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }) // 10:30, der nächste
buchung('p1', { startsAt: '2026-10-17T12:00:00.000Z' }) // 14:00
buchung('p1', { status: 'abgesagt', startsAt: '2026-10-17T09:00:00.000Z' })
buchung('p1', { quelle: 'sperre', startsAt: '2026-10-17T09:30:00.000Z' })
buchung('p1', { status: 'angefragt', startsAt: '2026-10-17T10:00:00.000Z' })
buchung('p1', { startsAt: '2026-10-18T08:00:00.000Z' }) // morgen
buchung('p1', { startsAt: '2026-10-16T21:30:00.000Z' }) // 23:30 Berlin, gestern
buchung('p2', { startsAt: '2026-10-17T07:00:00.000Z' }) // andere Website
const b = await holen()
assert.equal(info(b, 'p1', 'termine'), '3 Termine heute · nächster 10:30')
assert.equal(info(b, 'p2', 'termine'), '1 Termin heute')
})
test('Terminplaner: ein Termin, alle vorbei, keiner', async () => {
funktion('p1', 'termine')
assert.equal(info(await holen(), 'p1', 'termine'), 'Heute keine Termine')
buchung('p1', { startsAt: '2026-10-17T11:00:00.000Z' })
assert.equal(info(await holen(), 'p1', 'termine'), '1 Termin heute · nächster 13:00')
await leere('termineBookings')
buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T05:00:00.000Z' })
buchung('p1', { status: 'erledigt', startsAt: '2026-10-17T06:00:00.000Z' })
assert.equal(info(await holen(), 'p1', 'termine'), '2 Termine heute')
})
test('Terminplaner: der Tag ist der Berliner Tag, auch kurz nach Mitternacht (UTC liegt noch beim Vortag)', async () => {
funktion('p1', 'termine')
einschaltenDeps.jetzt = () => new Date('2026-10-17T22:30:00.000Z') // 00:30 Berlin am 18.10.
buchung('p1', { startsAt: '2026-10-17T21:59:00.000Z' }) // 23:59 am 17.10., nicht heute
buchung('p1', { startsAt: '2026-10-17T22:15:00.000Z' }) // 00:15 am 18.10., zählt, vorbei
buchung('p1', { startsAt: '2026-10-18T06:00:00.000Z' }) // 08:00, nächster
buchung('p1', { startsAt: '2026-10-18T22:00:00.000Z' }) // 00:00 am 19.10., nicht heute
assert.equal(info(await holen(), 'p1', 'termine'), '2 Termine heute · nächster 08:00')
})
/* ------------------------------------------------------------------------------------------ Webshop --- */
test('Webshop: neue (bezahlte, noch nicht bearbeitete) Bestellungen dieses Shops', async () => {
funktion('p1', 'shop'); projekt('p2'); funktion('p2', 'shop')
assert.equal(info(await holen(), 'p1', 'shop'), 'Keine offenen Bestellungen')
bestellung('p1', 'bezahlt'); bestellung('p1', 'bezahlt')
bestellung('p1', 'versendet'); bestellung('p1', 'offen'); bestellung('p1', 'abgeschlossen'); bestellung('p1', 'storniert')
bestellung('p2', 'bezahlt')
let b = await holen()
assert.equal(info(b, 'p1', 'shop'), '2 neue Bestellungen')
assert.equal(info(b, 'p2', 'shop'), '1 neue Bestellung')
})
/* ------------------------------------------------------------------------------------------ News --- */
test('News-Pinnwand: sichtbare Beiträge (nicht abgenommen, begonnen, nicht abgelaufen) dieser Website', async () => {
funktion('p1', 'news'); projekt('p2'); funktion('p2', 'news')
assert.equal(info(await holen(), 'p1', 'news'), 'Keine Beiträge sichtbar')
zettel('p1') // ohne Ende
zettel('p1', { visibleFrom: inStd(-24), visibleUntil: inStd(72) })
zettel('p1', { visibleFrom: inStd(-240), visibleUntil: inStd(1) })
zettel('p1', { visibleFrom: inStd(-120), visibleUntil: inStd(480) })
zettel('p1', { visibleFrom: inStd(-240), visibleUntil: inStd(-1) }) // abgelaufen
zettel('p1', { takenDownAt: inStd(-2) }) // abgenommen
zettel('p1', { visibleFrom: inStd(24) }) // beginnt erst morgen
zettel('p2')
const b = await holen()
assert.equal(info(b, 'p1', 'news'), '4 Beiträge sichtbar')
assert.equal(info(b, 'p2', 'news'), '1 Beitrag sichtbar')
})
/* ------------------------------------------------------------------------------------------ Buchungskalender --- */
test('Buchungskalender: offene Anfragen, sonst die nächste Anreise, sonst keine offenen Anfragen', async () => {
funktion('p1', 'unterkunft'); projekt('p2'); funktion('p2', 'unterkunft')
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Keine offenen Anfragen')
anfrage('p1', 'gebucht', '2026-11-02', '2026-11-05')
anfrage('p1', 'bezahlt', '2026-10-24', '2026-10-27')
anfrage('p1', 'gebucht', '2026-10-01', '2026-10-05') // vorbei
anfrage('p1', 'gebucht', '2026-10-15', '2026-10-19') // läuft schon: keine Anreise mehr
anfrage('p1', 'freigegeben', '2026-10-20', '2026-10-22') // noch nicht gebucht
anfrage('p1', 'abgelehnt', '2026-10-18', '2026-10-20')
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Nächste Anreise 24.10.')
anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
anfrage('p2', 'neu', '2026-12-01', '2026-12-03')
anfrage('p2', 'neu', '2026-12-05', '2026-12-07')
const b = await holen()
assert.equal(info(b, 'p1', 'unterkunft'), '1 offene Anfrage')
assert.equal(info(b, 'p2', 'unterkunft'), '2 offene Anfragen')
})
test('Buchungskalender: Anreise heute und morgen werden ausgeschrieben', async () => {
funktion('p1', 'unterkunft')
anfrage('p1', 'gebucht', '2026-10-18', '2026-10-20')
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Anreise morgen')
anfrage('p1', 'bezahlt', '2026-10-17', '2026-10-19')
assert.equal(info(await holen(), 'p1', 'unterkunft'), 'Anreise heute')
})
/* ------------------------------------------------------------------------------------------ Öffnungszeiten --- */
test('Öffnungszeiten live: der Status der Website von jetzt (ohne Zusatzabfrage), ohne Zeiten ein Hinweis', async () => {
funktion('p1', 'zeiten')
assert.equal(info(await holen(), 'p1', 'zeiten'), 'Noch keine Öffnungszeiten eingetragen')
zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'], 1: ['09:00', '18:00'] }))
resetZeitenCacheForTests()
aw.resetListCalls()
assert.equal(info(await holen(), 'p1', 'zeiten'), 'Jetzt geöffnet · schließt um 18 Uhr')
einschaltenDeps.jetzt = () => new Date('2026-10-17T18:00:00.000Z') // Samstag 20:00
assert.equal(info(await holen(), 'p1', 'zeiten'), 'Geschlossen · öffnet Montag um 9 Uhr')
})
/* ------------------------------------------------------------------------------------------ Regeln --- */
test('nur aktive Funktionen bekommen einen Text und eine Abfrage; alle anderen null', async () => {
for (const [key, state] of [['news', 'setup'], ['termine', 'ordered'], ['shop', 'disabled'], ['unterkunft', 'off'], ['zeiten', 'setup']]) funktion('p1', key, state)
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' })
bestellung('p1', 'bezahlt')
zettel('p1')
anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
zeitenDok('p1', zeitenDaten({ 6: ['09:00', '18:00'] }))
aw.resetListCalls()
const b = await holen()
for (const key of NAMEN) assert.equal(info(b, 'p1', key), null, key)
for (const key of NAMEN) assert.ok(Object.hasOwn(b.projects[0].features[key], 'kurzinfo'), `${key}: Feld da`)
for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung)
})
test('eine kleine feste Zahl schmaler Abfragen je aktiver Funktion: limit gesetzt, nur nötige Felder, nur Filter dieser Funktion', async () => {
alleAktiv('p1')
const gesamt = () => ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen'].map((s) => [s, abfragen(s)])
aw.resetListCalls()
await holen()
const [termine, shop, news, uk] = gesamt().map(([, a]) => a)
assert.equal(termine.length, 1)
assert.equal(shop.length, 1)
assert.equal(news.length, 1)
assert.equal(uk.length, 2)
const hat = (aufruf, methode) => aufruf.queries.find((q) => q.method === methode)
for (const aufruf of [...termine, ...news, ...uk]) {
const limit = hat(aufruf, 'limit')
assert.ok(limit && limit.values[0] <= 200, `limit ${JSON.stringify(limit)}`)
}
assert.deepEqual(hat(shop[0], 'limit').values, [1]) // nur die Anzahl (total)
assert.ok(hat(termine[0], 'select') && hat(news[0], 'select'))
assert.deepEqual(hat(termine[0], 'equal').values, ['f_p1_termine'])
assert.deepEqual(hat(shop[0], 'equal').values, ['f_p1_shop'])
assert.deepEqual(hat(news[0], 'equal').values, ['p1'])
for (const aufruf of uk) assert.deepEqual(hat(aufruf, 'equal').values, ['f_p1_unterkunft'])
assert.equal(aw.listCalls('oeffnungszeiten').length <= 1, true) // Zeiten: höchstens das Lesen für zeitenStatus, nichts zusätzlich
})
test('mehrere Websites: jede bekommt ihre eigene Zeile, die Abfragen laufen für alle gleichzeitig', async () => {
alleAktiv('p1'); projekt('p2'); alleAktiv('p2'); projekt('p3'); funktion('p3', 'termine')
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' })
buchung('p3', { startsAt: '2026-10-17T07:00:00.000Z' })
buchung('p3', { startsAt: '2026-10-17T13:00:00.000Z' })
const b = await holen()
assert.deepEqual(['p1', 'p2', 'p3'].map((p) => info(b, p, 'termine')).sort(), ['1 Termin heute · nächster 10:30', '2 Termine heute · nächster 15:00', 'Heute keine Termine'].sort())
assert.equal(info(b, 'p3', 'shop'), null)
})
test('Fehler einer Einzelabfrage ergibt null für diese Funktion; der Rest der Liste bleibt', async () => {
alleAktiv('p1')
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' })
bestellung('p1', 'bezahlt')
zettel('p1')
aw.failNext({ collection: 'termineBookings', method: 'GET', status: 500, message: 'geheimer Gästename im Fehlertext' })
const b = await holen()
assert.equal(info(b, 'p1', 'termine'), null)
assert.equal(info(b, 'p1', 'shop'), '1 neue Bestellung')
assert.equal(info(b, 'p1', 'news'), '1 Beitrag sichtbar')
assert.equal(b.modell, 'abo')
assert.ok(Array.isArray(b.katalog))
const log = console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n')
assert.match(log, /Kurzinfo \(termine\)/)
assert.doesNotMatch(log, /geheimer/)
// ein Teilfehler im Buchungskalender (eine der zwei Abfragen) ergibt null, nie eine halbe Aussage
anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
aw.failNext({ collection: 'ukAnfragen', method: 'GET', status: 500 })
assert.equal(info(await holen(), 'p1', 'unterkunft'), null)
})
test('Zeitüberschreitung ergibt null, ohne die Liste aufzuhalten', async () => {
alleAktiv('p1')
bestellung('p1', 'bezahlt'); zettel('p1')
kurzinfoDeps.limitMs = 60
const echt = globalThis.fetch
mock.method(globalThis, 'fetch', async (url, ...rest) => {
if (String(url).includes('/shopOrders/')) await new Promise((r) => setTimeout(r, 600))
return echt(url, ...rest)
})
const start = Date.now()
const b = await holen()
assert.ok(Date.now() - start < 550, `Antwort dauerte ${Date.now() - start} ms`)
assert.equal(info(b, 'p1', 'shop'), null)
assert.equal(info(b, 'p1', 'news'), '1 Beitrag sichtbar')
await new Promise((r) => setTimeout(r, 700)) // die späte Antwort richtet nichts mehr an
})
test('ohne neue Ansicht: keine Kurzinfo in der Antwort, keine einzige Abfrage dafür; sonst ist die neue Antwort die alte plus ergänzte Felder', async () => {
alleAktiv('p1')
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z' }); bestellung('p1', 'bezahlt'); zettel('p1'); anfrage('p1', 'neu', '2026-12-01', '2026-12-03')
config.kundenansichtNeu.mode = 'off'
aw.resetListCalls()
const alt = await holen()
assert.doesNotMatch(JSON.stringify(alt), /kurzinfo/)
for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung)
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k2@example.invalid'] // Kunde nicht in der Liste
assert.deepEqual(await holen(), alt)
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
const neu = await holen()
for (const key of ['modell', 'kundenart', 'abo', 'katalog']) delete neu[key]
for (const p of neu.projects) for (const f of Object.values(p.features)) { delete f.kurzinfo; delete f.anfrageStatus }
assert.deepEqual(neu, alt)
})
test('die Kurzinfo ist ein kurzer Text ohne Preis, Mail-Adresse oder Namen von Gästen', async () => {
alleAktiv('p1')
buchung('p1', { startsAt: '2026-10-17T08:30:00.000Z', gastName: 'Erika Beispiel', gastEmail: 'erika@example.invalid', preisCent: 4500 })
bestellung('p1', 'bezahlt', { kaeuferEmail: 'kauf@example.invalid', summeCent: 9900 })
zettel('p1', { title: 'Geheimer Titel' })
anfrage('p1', 'neu', '2026-12-01', '2026-12-03', { gast: JSON.stringify({ name: 'Max Gast' }) })
const b = await holen()
const texte = NAMEN.map((k) => info(b, 'p1', k))
for (const t of texte) {
assert.equal(typeof t, 'string')
assert.ok(t.length > 0 && t.length <= 60, t)
assert.doesNotMatch(t, /€|Euro|@|Erika|Max|Geheimer|\d+,\d\d/)
}
})