Files
ClaudeandClaude Opus 5.5 e87140e5ba Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.

Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.

Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:12:23 +00:00

77 lines
3.9 KiB
JavaScript

import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Protokoll der Kundenansicht (Collection customerViewLog): schreiben, lesen, nach 12 Monaten loeschen
const aw = await startAppwriteMock({ schema: { customerViewLog: schemaKeys('customerViewLog') } })
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { VIEW_ACTIONS, logViewAction, listViewLog, purgeViewLog } = await import('../server/services/customerViewLog.js')
after(() => Promise.all([aw.close(), st.close()]))
const DAY_MS = 24 * 60 * 60 * 1000
const entries = () => [...aw.docs('customerViewLog').values()]
const seedLog = (id, customerId, createdAt, action = 'geoeffnet') =>
aw.seed('customerViewLog', { $id: id, customerId, staffUserId: 'u_staff2', staffName: 'Andrej', action, detail: '', createdAt })
test('VIEW_ACTIONS: genau die zwölf Aktionen', () => {
assert.deepEqual(VIEW_ACTIONS, [
'geoeffnet', 'beendet', 'abgelaufen', 'news_angelegt', 'news_bild', 'news_geaendert', 'news_geloescht',
'termin_einstellungen', 'termin_aktion', 'termin_manuell', 'shop_aenderung', 'zeiten_gespeichert',
])
})
test('logViewAction schreibt alle Felder und kuerzt detail auf 300', async () => {
const before = entries().length
await logViewAction({ customerId: 'c9', staffUserId: 'u_staff2', staffName: 'Andrej', action: 'news_angelegt', detail: 'x'.repeat(400) })
const all = entries()
assert.equal(all.length, before + 1)
const doc = all.find((d) => d.customerId === 'c9')
assert.equal(doc.detail.length, 300)
assert.equal(doc.action, 'news_angelegt')
assert.equal(doc.staffName, 'Andrej')
assert.equal(doc.staffUserId, 'u_staff2')
assert.ok(Math.abs(Date.parse(doc.createdAt) - Date.now()) < 5000)
})
test('logViewAction wirft nicht, wenn Appwrite scheitert', async (t) => {
const warn = t.mock.method(console, 'warn', () => {})
aw.failNext({ collection: 'customerViewLog', method: 'POST', status: 500, message: 'kaputt' })
await assert.doesNotReject(() => logViewAction({ customerId: 'c8', staffUserId: 'u', staffName: 'X', action: 'geoeffnet' }))
assert.equal(entries().filter((d) => d.customerId === 'c8').length, 0)
assert.equal(warn.mock.callCount(), 1)
})
test('listViewLog: nur dieser Kunde, neueste zuerst, Form {id, at, staffName, action, detail}', async () => {
seedLog('l1', 'c1', '2026-09-01T10:00:00.000Z')
seedLog('l2', 'c1', '2026-09-03T10:00:00.000Z', 'news_angelegt')
seedLog('l3', 'c1', '2026-09-02T10:00:00.000Z', 'beendet')
seedLog('l4', 'c2', '2026-09-04T10:00:00.000Z')
const list = await listViewLog('c1')
assert.deepEqual(list.map((e) => e.id), ['l2', 'l3', 'l1'])
assert.deepEqual(list[0], { id: 'l2', at: '2026-09-03T10:00:00.000Z', staffName: 'Andrej', action: 'news_angelegt', detail: '' })
})
test('purgeViewLog loescht nur Eintraege aelter als 365 Tage', async () => {
const now = new Date('2026-10-01T12:00:00.000Z')
seedLog('p1', 'c3', new Date(now - 366 * DAY_MS).toISOString())
seedLog('p2', 'c3', new Date(now - 364 * DAY_MS).toISOString())
seedLog('p3', 'c3', new Date(now - 1 * DAY_MS).toISOString())
const { deleted } = await purgeViewLog({ now })
assert.equal(deleted, 1)
assert.deepEqual(entries().filter((d) => d.customerId === 'c3').map((d) => d.$id).sort(), ['p2', 'p3'])
})
test('purgeViewLog arbeitet seitenweise (250 alte Eintraege -> deleted 250)', async () => {
const now = new Date('2026-10-01T12:00:00.000Z')
const old = new Date(now - 400 * DAY_MS).toISOString()
for (let i = 0; i < 250; i += 1) seedLog(`old${i}`, 'c4', old)
seedLog('keep', 'c4', now.toISOString())
const { deleted } = await purgeViewLog({ now })
assert.equal(deleted, 250)
assert.deepEqual(entries().filter((d) => d.customerId === 'c4').map((d) => d.$id), ['keep'])
})