Files
Webklar-Kundenbereich/test/kundenansichtKurzinfo.test.js
T
ClaudeandClaude Sonnet 5.5 6bfcaa892c Kundenansicht neu (Meine): Kurzinfo nur auf Anfrage (GET /api/funktionen?kurzinfo=1)
Der Server berechnet kurzinfo (Terminplaner, Webshop, News, Buchungskalender, Öffnungszeiten) nur noch,
wenn die neue Ansicht ?kurzinfo=1 sendet. Das tut allein die Seite "Meine"; Etagen, Einschalt-Ablauf und
Module fragen ohne den Parameter, dann fehlt das Feld und es gibt keine Abfrage auf Termine, Bestellungen,
Beiträge und Anfragen. Mit dem Parameter bleibt die Antwort wie bisher (Text oder null je Funktion).
Nur der Wert "1" gilt; doppelte oder als Liste gesendete Parameter heißen: ohne. Alte Ansicht unverändert.

Tests: kundenansichtKurzinfo (neuer Fall ohne/mit Parameter, Abfragen und Feld), knFunktionenAnsicht
(die Seite lädt genau einmal ?kurzinfo=1, auch nach "Nochmal versuchen").

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

385 lines
23 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?kurzinfo=1', { cookie })) // so fragt „Meine“ (kurzinfo nur auf Anfrage)
const holenOhne = async (pfad = '/api/funktionen', cookie = c1) => (await get(pfad, { cookie })) // Etagen, Einschalt-Ablauf, Module
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() // auch mit ?kurzinfo=1 ändert die alte Ansicht nichts
assert.doesNotMatch(JSON.stringify(alt), /kurzinfo/)
for (const sammlung of ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']) assert.equal(abfragen(sammlung).length, 0, sammlung)
assert.deepEqual(await holenOhne(), alt)
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)
})
/* ------------------------------------------------------------------------------------ nur auf Anfrage --- */
test('Kurzinfo nur auf Anfrage: ohne ?kurzinfo=1 fehlt das Feld überall und es gibt keine einzige Abfrage dafür; der Rest der Antwort ist gleich', async () => {
alleAktiv('p1'); projekt('p2'); funktion('p2', 'termine'); funktion('p2', 'news', 'setup')
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'] }))
const sammlungen = ['termineBookings', 'shopOrders', 'newsPosts', 'ukAnfragen']
for (const pfad of ['/api/funktionen', '/api/funktionen?kurzinfo=0', '/api/funktionen?kurzinfo=', '/api/funktionen?kurzinfo=true', '/api/funktionen?kurzinfo=1&kurzinfo=0', '/api/funktionen?kurzinfo[]=1', '/api/funktionen?x=1']) {
aw.resetListCalls()
const ohne = await holenOhne(pfad)
assert.doesNotMatch(JSON.stringify(ohne), /kurzinfo/, pfad)
assert.equal(ohne.modell, 'abo', pfad) // die neue Ansicht bekommt sonst alles wie bisher
for (const sammlung of sammlungen) assert.equal(abfragen(sammlung).length, 0, `${pfad}: ${sammlung}`)
}
aw.resetListCalls()
const mit = await holen()
for (const sammlung of sammlungen) assert.ok(abfragen(sammlung).length > 0, sammlung)
assert.equal(info(mit, 'p1', 'termine'), '1 Termin heute · nächster 10:30')
assert.equal(info(mit, 'p2', 'news'), null) // mit Anfrage haben auch Funktionen ohne Text das Feld (null)
const ohne = await holenOhne()
for (const p of mit.projects) for (const f of Object.values(p.features)) delete f.kurzinfo
assert.deepEqual(ohne, mit)
})
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/)
}
})