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>
77 lines
3.9 KiB
JavaScript
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'])
|
|
})
|