Files
Webklar-Kundenbereich/test/siteFeatures.test.js
T
ClaudeandClaude Opus 5.5 0e74f1669c Webshop A3: Register-Eintrag shop, Haken, Rechtstext-Satz und Ticket-Details
Registereintrag config.features.shop (Bestelltexte mit Fassung config.shop.fassung, Rechtsunterlagen-Getter,
Mailtexte, Ticket), STANDARD_HAKEN.shop (Schalter, Bestellpruefung ueber shop/anfrage.js, Unterlagen, Belehrung),
legalDocs-Satz shop, shopTicketDetails sowie Rumpf von anfrageOffenePunkte. Tests angepasst und um Shop-Faelle erweitert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 08:35:28 +00:00

739 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after, mock } 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'
// websiteProjects, workorders und customers legt das Setup-Skript nicht an: ohne Schema im Nachbau
const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') } })
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const sf = await import('../server/services/siteFeatures.js')
const { LOESCHFRIST_TAGE, loeschzeit, loeschTag } = await import('../server/services/termine/frist.js')
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin' })
aw.seed('customers', { $id: 'c2', name: 'Kunde Zwei' })
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', liveDomain: 'baeckerei.de' })
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Café', subdomain: 'cafe' })
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c2', projectName: 'Fremd' })
aw.seed('websiteProjects', { $id: 'p_' + 'x'.repeat(40), customerId: 'c1', projectName: 'Lang' })
const consent = { text: 'Ich stimme zu', at: '2026-09-30T10:00:00.000Z', ip: '1.2.3.4', userAgent: 'UA' }
const tickets = () => [...aw.docs('workorders').values()].filter((t) => String(t.topic).startsWith('News-Pinnwand einbauen'))
test('ohne_dokument_ist_off', async () => {
const list = await sf.listCustomerFeatures('c1')
assert.equal(list.length, 3)
for (const p of list) assert.equal(p.features.news.state, 'off')
assert.equal(list.find((p) => p.projectId === 'p1').liveUrl, 'https://baeckerei.de')
})
test('C1: eine neue Bestellung leert IP und Browser aus einer frueheren Bestellung desselben Dokuments', async () => {
const first = await sf.markOrdered({ customerId: 'c2', projectId: 'p3', key: 'news', sessionId: 'cs_c1_a', consent })
// Altbestand: eine frueher gespeicherte IP am Dokument (vor dieser Aenderung wurde sie geschrieben)
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
await updateDocument('portalFeatures', first.$id, { consentIp: '9.9.9.9', consentUserAgent: 'alt', state: 'off', enabled: false })
const again = await sf.markOrdered({ customerId: 'c2', projectId: 'p3', key: 'news', sessionId: 'cs_c1_b', consent })
assert.deepEqual([again.consentIp, again.consentUserAgent], ['', ''])
assert.equal(aw.docs('portalFeatures').get(first.$id).consentIp, '')
aw.docs('portalFeatures').delete(first.$id) // die uebrigen Faelle dieser Datei zaehlen Dokumente
})
test('bezahlt_legt_ticket_an', async () => {
const ordered = await sf.markOrdered({ customerId: 'c1', projectId: 'p1', key: 'news', sessionId: 'cs_1', consent })
assert.equal(ordered.state, 'ordered')
assert.equal(ordered.enabled, false)
// C1: IP und Browser der Bestellung stehen nur im Nachweis (featureConsents), nie am Funktionsdokument, auch wenn der Aufrufer sie mitgibt
assert.equal(ordered.consentIp, '')
assert.equal(ordered.consentUserAgent, '')
assert.equal(ordered.consentText, 'Ich stimme zu')
assert.equal(ordered.consentAt, '2026-09-30T10:00:00.000Z')
assert.match(ordered.siteKey, /^[a-z2-7]{16}$/)
const { doc, created } = await sf.markPaid({ sessionId: 'cs_1', invoiceId: 'in_1', invoiceUrl: 'https://x/inv' })
assert.equal(created, true)
assert.equal(doc.state, 'setup')
assert.equal(doc.enabled, false)
assert.equal(doc.unlockedByPurchase, true)
assert.ok(doc.ticketId)
assert.equal(tickets().length, 1)
assert.match(tickets()[0].topic, /^News-Pinnwand einbauen – Bäckerei/)
assert.ok(tickets()[0].details.includes(`data-webklar-news="${doc.siteKey}"`))
const view = (await sf.listCustomerFeatures('c1')).find((p) => p.projectId === 'p1').features.news
assert.equal(view.state, 'setup')
assert.ok(view.ticketWoid)
})
test('webhook_idempotent', async () => {
const again = await sf.markPaid({ sessionId: 'cs_1', invoiceId: 'in_1', invoiceUrl: 'https://x/inv' })
assert.equal(again.created, false)
assert.equal(tickets().length, 1)
})
test('ticket_geschlossen_aktiviert', async () => {
const doc = await sf.getFeatureDoc('c1', 'p1', 'news')
assert.deepEqual(await sf.activateClosedTickets({ customerId: 'c1' }), [])
assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).state, 'setup')
aw.seed('workorders', { ...aw.docs('workorders').get(doc.ticketId), status: 'Closed' })
assert.deepEqual(await sf.activateClosedTickets({ customerId: 'c2' }), [])
const now = new Date('2026-10-01T08:00:00Z')
const done = await sf.activateClosedTickets({ now })
assert.equal(done.length, 1)
assert.equal(done[0].state, 'active')
assert.equal(done[0].enabled, true)
assert.equal(done[0].activatedAt, now.toISOString())
assert.equal((await sf.findActiveBySiteKey(doc.siteKey)).$id, doc.$id)
})
test('fehlgeschlagene_zahlung_setzt_off', async () => {
await sf.markOrdered({ customerId: 'c1', projectId: 'p2', key: 'news', sessionId: 'cs_2', consent })
const doc = await sf.markCheckoutFailed('cs_2')
assert.equal(doc.state, 'off')
assert.equal(doc.enabled, false)
assert.equal(await sf.markCheckoutFailed('cs_unbekannt'), null)
})
test('fremdes_projekt_403', async () => {
await assert.rejects(() => sf.assertOwnProject('c1', 'p3'), { status: 403 })
await assert.rejects(() => sf.assertOwnProject('c1', 'gibts_nicht'), { status: 403 })
assert.equal((await sf.assertOwnProject('c1', 'p1')).$id, 'p1')
})
test('sitekey_format', () => {
const keys = new Set()
for (let i = 0; i < 100; i += 1) {
const k = sf.newSiteKey()
assert.match(k, /^[a-z2-7]{16}$/)
keys.add(k)
}
assert.equal(keys.size, 100)
})
test('admin_aktiviert_ohne_zahlung', async () => {
const before = tickets().length
const doc = await sf.adminActivate('c1', 'p2', 'news', { by: 'kenso' })
assert.equal(doc.state, 'active')
assert.equal(doc.enabled, true)
assert.equal(doc.unlockedByPurchase, false)
assert.equal(tickets().length, before)
const off = await sf.disable(doc.$id, { by: 'kenso' })
assert.equal(off.state, 'disabled')
assert.equal(off.enabled, false)
})
test('lange_projekt_id_bekommt_gueltige_dokument_id', async () => {
const doc = await sf.adminActivate('c1', 'p_' + 'x'.repeat(40), 'news', {})
assert.ok(doc.$id.length <= 36)
})
test('bestellen_bei_aktiver_funktion_409', async () => {
const doc = await sf.adminActivate('c1', 'p1', 'news', {}).catch(() => null)
const active = doc || (await sf.getFeatureDoc('c1', 'p1', 'news'))
assert.equal(active.state, 'active')
await assert.rejects(
() => sf.markOrdered({ customerId: 'c1', projectId: 'p1', key: 'news', sessionId: 'cs_x', consent }),
{ status: 409 }
)
assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).state, 'active')
})
test('parallele_webhooks_ein_ticket', async () => {
aw.seed('websiteProjects', { $id: 'p4', customerId: 'c1', projectName: 'Parallel' })
await sf.markOrdered({ customerId: 'c1', projectId: 'p4', key: 'news', sessionId: 'cs_par', consent })
const before = tickets().length
const res = await Promise.all([1, 2, 3].map(() => sf.markPaid({ sessionId: 'cs_par', invoiceId: 'in_p', invoiceUrl: 'u' })))
assert.equal(res.filter((r) => r.created).length, 1)
assert.equal(tickets().length, before + 1)
assert.ok((await sf.getFeatureDoc('c1', 'p4', 'news')).ticketId)
})
test('abgelaufen_fasst_setup_und_active_nicht_an', async () => {
// cs_1 ist bezahlt ('setup' bzw. nach dem Schliessen des Tickets 'active'): ein spaetes expired aendert nichts
const before = await sf.getFeatureDoc('c1', 'p1', 'news')
assert.notEqual(before.state, 'ordered')
const after = await sf.markCheckoutFailed('cs_1')
assert.equal(after.state, before.state)
assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).state, before.state)
assert.equal((await sf.getFeatureDoc('c1', 'p1', 'news')).purchaseStatus, 'paid')
})
test('bezahlt_und_abgelaufen_gleichzeitig_bleiben_stimmig', async () => {
const pid = 'p_race'
aw.seed('websiteProjects', { $id: pid, customerId: 'c1', projectName: 'Rennen' })
await sf.markOrdered({ customerId: 'c1', projectId: pid, key: 'news', sessionId: 'cs_race', consent })
const ticketsBefore = tickets().length
await Promise.all([
sf.markPaid({ sessionId: 'cs_race', invoiceId: 'in_r', invoiceUrl: '' }),
sf.markCheckoutFailed('cs_race'),
])
const doc = await sf.getFeatureDoc('c1', pid, 'news')
// entweder bezahlt (mit Ticket) oder zurueckgesetzt (ohne): nie 'setup' ohne Ticket oder 'off' mit Ticket
if (doc.state === 'setup') {
assert.ok(doc.ticketId)
assert.equal(tickets().length, ticketsBefore + 1)
} else {
assert.equal(doc.state, 'off')
assert.equal(doc.ticketId || '', '')
assert.equal(tickets().length, ticketsBefore)
}
})
// ---------------------------------------------------------------- Terminplaner als zweite Funktion ---
test('termine_ist_zweite_funktion: Schluessel und Bezeichnungen', () => {
// erst hier angelegt: die aelteren Tests oben zaehlen die Projekte von c1
aw.seed('websiteProjects', { $id: 'pt1', customerId: 'c1', projectName: 'Salon' })
aw.seed('websiteProjects', { $id: 'pt2', customerId: 'c1', projectName: 'Studio' })
aw.seed('websiteProjects', { $id: 'pt3', customerId: 'c2', projectName: 'Fremder Salon' })
assert.deepEqual(sf.FEATURE_KEYS, ['news', 'termine', 'shop'])
assert.deepEqual(sf.FEATURE_LABELS, {
news: { name: 'News-Pinnwand', keine: 'keine News-Pinnwand' },
termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' },
shop: { name: 'Webshop', keine: 'keinen Webshop' },
})
})
test('liste_zeigt_termine_je_projekt_ohne_dokument_als_off', async () => {
const list = await sf.listCustomerFeatures('c1')
const p = list.find((x) => x.projectId === 'pt1')
assert.deepEqual(Object.keys(p.features), ['news', 'termine', 'shop'])
assert.equal(p.features.termine.state, 'off')
assert.equal(p.features.termine.siteKey, null)
assert.equal(p.features.termine.ticketWoid, null)
})
test('termine_wird_aktiviert_und_bekommt_eigenen_sitekey_und_eigenes_dokument', async () => {
const doc = await sf.adminActivate('c1', 'pt1', 'termine', { by: 'kenso' })
assert.equal(doc.featureKey, 'termine')
assert.equal(doc.state, 'active')
assert.equal(doc.enabled, true)
assert.equal(doc.unlockedByPurchase, false)
assert.match(doc.siteKey, /^[a-z2-7]{16}$/)
assert.equal(await sf.getFeatureDoc('c1', 'pt1', 'news'), null, 'News bleibt unberuehrt')
const view = (await sf.listCustomerFeatures('c1')).find((x) => x.projectId === 'pt1').features
assert.equal(view.termine.state, 'active')
assert.equal(view.termine.siteKey, doc.siteKey)
assert.equal(view.news.state, 'off')
await assert.rejects(() => sf.adminActivate('c1', 'pt1', 'kalender', {}), { status: 400 })
})
test('findActiveBySiteKey: Vorgabe ist news, termine nur mit zweitem Argument, nie der andere Schluessel', async () => {
const termine = await sf.getFeatureDoc('c1', 'pt1', 'termine')
assert.equal((await sf.findActiveBySiteKey(termine.siteKey, 'termine')).$id, termine.$id)
assert.equal(await sf.findActiveBySiteKey(termine.siteKey), null, 'ohne zweites Argument = news')
assert.equal(await sf.findActiveBySiteKey(termine.siteKey, 'news'), null)
const news = await sf.adminActivate('c1', 'pt2', 'news', { by: 'kenso' })
assert.equal((await sf.findActiveBySiteKey(news.siteKey)).$id, news.$id)
assert.equal((await sf.findActiveBySiteKey(news.siteKey, 'news')).$id, news.$id)
assert.equal(await sf.findActiveBySiteKey(news.siteKey, 'termine'), null)
assert.equal(await sf.findActiveBySiteKey('zzzzzzzzzzzzzzzz', 'termine'), null)
assert.equal(await sf.findActiveBySiteKey('KURZ', 'termine'), null)
})
test('abgeschaltet_ist_nicht_auffindbar_und_wieder_aktivieren_leert_disabledAt (R36)', async () => {
const doc = await sf.getFeatureDoc('c1', 'pt1', 'termine')
const off = await sf.disable(doc.$id, { by: 'kenso' })
assert.equal(off.state, 'disabled')
assert.ok(Math.abs(Date.parse(off.disabledAt) - Date.now()) < 5000)
assert.equal(await sf.findActiveBySiteKey(doc.siteKey, 'termine'), null)
const on = await sf.activate(doc.$id, { by: 'kenso' })
assert.equal(on.state, 'active')
assert.equal(on.disabledAt, null, 'sonst koennte der 30-Tage-Loeschlauf eine wieder eingeschaltete Funktion treffen')
assert.equal((await sf.getFeatureDoc('c1', 'pt1', 'termine')).disabledAt, null)
assert.equal((await sf.findActiveBySiteKey(doc.siteKey, 'termine')).$id, doc.$id)
})
test('activate gilt fuer jede Funktion gleich: News verliert disabledAt ebenfalls', async () => {
const news = await sf.getFeatureDoc('c1', 'pt2', 'news')
assert.equal((await sf.disable(news.$id, {})).state, 'disabled')
assert.equal((await sf.activate(news.$id, {})).disabledAt, null)
})
test('activateClosedTickets: Funktionen mit ticketFreischaltung; News verhaelt sich unveraendert (TP2)', async () => {
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { config } = await import('../server/config.js')
const seedSetup = (id, featureKey, siteKey, ticketId) => {
aw.seed('portalFeatures', { $id: id, customerId: 'c1', projectId: 'pt2', featureKey, state: 'setup', enabled: false, siteKey, ticketId })
aw.seed('workorders', { $id: ticketId, woid: 'WO-9', status: 'Closed' })
}
seedSetup('f_setup_termine', 'termine', 'bbbbbbbbbbbbbbbb', 't_closed_termine')
seedSetup('f_setup_news', 'news', 'cccccccccccccccc', 't_closed_news')
seedSetup('f_setup_analyse', 'analyse', 'dddddddddddddddd', 't_closed_analyse')
const ids = ['f_setup_termine', 'f_setup_news', 'f_setup_analyse']
const reg = await import('../server/services/featureRegistry.js')
try {
assert.deepEqual(reg.ticketFreischaltungKeys(), ['news', 'termine', 'shop'])
// News wie bisher; Termine wartet auf Einstellung und Abo (Haken freischaltbar); Analyse hat keine ticketFreischaltung
const done = await sf.activateClosedTickets({ customerId: 'c1' })
assert.deepEqual(done.map((d) => d.featureKey), ['news'])
assert.equal(aw.docs('portalFeatures').get('f_setup_news').state, 'active')
assert.equal(aw.docs('portalFeatures').get('f_setup_termine').state, 'setup')
assert.match(aw.docs('portalFeatures').get('f_setup_termine').adminNote, /Freischaltung wartet/)
assert.equal(aw.docs('portalFeatures').get('f_setup_analyse').state, 'setup')
assert.ok(!aw.docs('portalFeatures').get('f_setup_analyse').adminNote)
// Sind die Voraussetzungen der Funktion erfuellt (Haken), oeffnet derselbe Lauf auch Termine - Analyse bleibt zu
config.features.termine.freischaltbar = async () => true
const spaeter = await sf.activateClosedTickets({ customerId: 'c1' })
assert.deepEqual(spaeter.map((d) => d.featureKey), ['termine'])
assert.equal(aw.docs('portalFeatures').get('f_setup_analyse').state, 'setup')
} finally {
delete config.features.termine.freischaltbar
for (const id of ids) await deleteDocument(config.collections.portalFeatures, id)
}
})
test('disableCustomerFeatures: schaltet nur die genannte Funktion des Kunden ab, mit Notiz', async () => {
const mine = await sf.getFeatureDoc('c1', 'pt1', 'termine')
const other = await sf.adminActivate('c2', 'pt3', 'termine', { by: 'kenso' })
const news = await sf.getFeatureDoc('c1', 'pt2', 'news')
await sf.activate(news.$id, {})
const done = await sf.disableCustomerFeatures('c1', 'termine', { adminNote: 'Kunde gelöscht' })
assert.deepEqual(done.map((d) => d.$id), [mine.$id])
const after = await sf.getFeatureDoc('c1', 'pt1', 'termine')
assert.equal(after.state, 'disabled')
assert.equal(after.enabled, false)
assert.equal(after.adminNote, 'Kunde gelöscht')
assert.ok(after.disabledAt)
assert.equal((await sf.getFeatureDoc('c2', 'pt3', 'termine')).state, 'active', 'anderer Kunde')
assert.equal(other.state, 'active')
assert.equal((await sf.getFeatureDoc('c1', 'pt2', 'news')).state, 'active', 'andere Funktion')
// schon abgeschaltet: disabledAt (Beginn der 30 Tage) bleibt
const marker = after.disabledAt
assert.deepEqual(await sf.disableCustomerFeatures('c1', 'termine', { adminNote: 'Kunde gelöscht' }), [])
assert.equal((await sf.getFeatureDoc('c1', 'pt1', 'termine')).disabledAt, marker)
await assert.rejects(() => sf.disableCustomerFeatures('c1', 'kalender', {}), { status: 400 })
})
// ------------------------------------------------ disable(): Grund und Idempotenz (A11) ---
const PF = '/collections/portalFeatures/documents'
const termineDoc = (id, over = {}) => ({ $id: id, customerId: 'c1', projectId: `p_${id}`.slice(0, 36), featureKey: 'termine', state: 'active', enabled: true, siteKey: 'abcdefghijklmnop', ...over })
const lesen = (id) => aw.docs('portalFeatures').get(id)
const warte = (ms = 5) => new Promise((resolve) => setTimeout(resolve, ms))
/** Fuehrt fn aus und zaehlt die schreibenden Anfragen an portalFeatures (PATCH, POST, DELETE). */
async function schreibzugriffe(fn) {
const echt = globalThis.fetch
let schreibend = 0
const spion = mock.method(globalThis, 'fetch', async (url, opts = {}) => {
if (String(url).includes(PF) && String(opts.method || 'GET').toUpperCase() !== 'GET') schreibend += 1
return echt(url, opts)
})
try {
const ergebnis = await fn()
return { ergebnis, schreibend }
} finally {
spion.mock.restore()
}
}
test('DISABLED_REASONS ist die feste Liste aus dem Plankopf', () => {
assert.deepEqual(sf.DISABLED_REASONS, ['team', 'abo_ende', 'abo_ende_vor_einrichtung', 'kunde_geloescht'])
})
test('disable ohne Grund speichert team, mit Grund den Grund, ein unbekannter Grund ist 400 ohne Schreibzugriff', async () => {
aw.seed('portalFeatures', termineDoc('f_a11_grund0'))
const ohne = await sf.disable('f_a11_grund0', { by: 'kenso' })
assert.equal(ohne.state, 'disabled')
assert.equal(ohne.disabledReason, 'team')
assert.equal(ohne.adminNote, 'Abgeschaltet von kenso')
for (const grund of sf.DISABLED_REASONS) {
aw.seed('portalFeatures', termineDoc(`f_a11_${grund}`))
const doc = await sf.disable(`f_a11_${grund}`, { reason: grund })
assert.equal(doc.disabledReason, grund, grund)
assert.equal(lesen(doc.$id).disabledReason, grund, `${grund}: gespeichert`)
}
const ohneNotiz = lesen('f_a11_abo_ende')
assert.equal(ohneNotiz.adminNote, undefined, 'ohne by und adminNote keine Notiz (wie bisher)')
aw.seed('portalFeatures', termineDoc('f_a11_frist'))
for (const falsch of ['frist', '', null, 'TEAM']) {
const { schreibend } = await schreibzugriffe(() => assert.rejects(() => sf.disable('f_a11_frist', { reason: falsch }), { status: 400 }))
assert.equal(schreibend, 0, `Grund ${JSON.stringify(falsch)}: kein Schreibzugriff`)
}
const unberuehrt = lesen('f_a11_frist')
assert.equal(unberuehrt.state, 'active')
assert.equal(unberuehrt.enabled, true)
assert.equal(unberuehrt.disabledAt, undefined)
assert.equal(unberuehrt.disabledReason, undefined)
})
test('disable eines unbekannten Dokuments bleibt 404', async () => {
await assert.rejects(() => sf.disable('f_a11_gibtsnicht', { reason: 'abo_ende' }), { status: 404 })
})
test('disable ist idempotent: das zweite Mal bleibt alles, es gibt keinen Schreibzugriff und das Dokument kommt zurueck', async () => {
aw.seed('portalFeatures', termineDoc('f_a11_idem'))
const erst = await sf.disable('f_a11_idem', { reason: 'abo_ende', adminNote: 'Abo beendet' })
const davor = lesen('f_a11_idem')
await warte()
const { ergebnis: zweit, schreibend } = await schreibzugriffe(() => sf.disable('f_a11_idem', { by: 'kenso', reason: 'kunde_geloescht', adminNote: 'Kunde gelöscht' }))
assert.equal(schreibend, 0, 'schon abgeschaltet: nichts wird geschrieben')
const danach = lesen('f_a11_idem')
assert.deepEqual(danach, davor, 'das ganze Dokument unveraendert')
assert.equal(danach.disabledAt, erst.disabledAt)
assert.equal(danach.disabledReason, 'abo_ende')
assert.equal(danach.adminNote, 'Abo beendet')
assert.equal(danach.$updatedAt, davor.$updatedAt)
assert.deepEqual(zweit, davor, 'liefert das (gelesene) Dokument')
// auch ohne Angaben, auch das dritte Mal
assert.deepEqual(await sf.disable('f_a11_idem', {}), davor)
})
test('disable: zwei gleichzeitige Aufrufe schreiben einmal, beide sehen dasselbe disabledAt und denselben Grund', async () => {
aw.seed('portalFeatures', termineDoc('f_a11_parallel'))
const { ergebnis, schreibend } = await schreibzugriffe(() =>
Promise.all([
sf.disable('f_a11_parallel', { reason: 'abo_ende', adminNote: 'zuerst' }),
sf.disable('f_a11_parallel', { reason: 'kunde_geloescht', adminNote: 'danach' }),
])
)
assert.equal(schreibend, 1)
const [a, b] = ergebnis
assert.equal(a.disabledAt, b.disabledAt)
assert.equal(b.disabledReason, 'abo_ende', 'der erste Aufruf gewinnt')
assert.equal(b.adminNote, 'zuerst')
assert.equal(lesen('f_a11_parallel').disabledAt, a.disabledAt)
// viele gleichzeitige Aufrufe (Doppelklick, Kundenloeschung und Abgleich zugleich)
aw.seed('portalFeatures', termineDoc('f_a11_parallel2'))
const viele = await schreibzugriffe(() => Promise.all([1, 2, 3, 4, 5].map(() => sf.disable('f_a11_parallel2', {}))))
assert.equal(viele.schreibend, 1)
assert.equal(new Set(viele.ergebnis.map((d) => d.disabledAt)).size, 1)
})
test('disable: Altbestand disabled ohne disabledAt bekommt nur disabledAt (jetzt), Grund und Notiz bleiben', async () => {
aw.seed('portalFeatures', termineDoc('f_a11_alt', { state: 'disabled', enabled: false, adminNote: 'Altbestand' }))
const { ergebnis, schreibend } = await schreibzugriffe(() => sf.disable('f_a11_alt', { reason: 'abo_ende', adminNote: 'neu' }))
assert.equal(schreibend, 1)
assert.ok(Math.abs(Date.parse(ergebnis.disabledAt) - Date.now()) < 5000, 'disabledAt ist jetzt')
assert.equal(ergebnis.adminNote, 'Altbestand')
assert.ok(!ergebnis.disabledReason, 'der Grund bleibt leer (Altbestand wie team behandelt, nie angefasst)')
assert.equal(ergebnis.state, 'disabled')
const gesetzt = lesen('f_a11_alt')
assert.equal(gesetzt.disabledAt, ergebnis.disabledAt)
assert.ok(!gesetzt.disabledReason)
// der zweite Aufruf schreibt nichts mehr
const { ergebnis: zweit, schreibend: nochmal } = await schreibzugriffe(() => sf.disable('f_a11_alt', { reason: 'kunde_geloescht', adminNote: 'x' }))
assert.equal(nochmal, 0)
assert.equal(zweit.disabledAt, gesetzt.disabledAt)
assert.deepEqual(lesen('f_a11_alt'), gesetzt)
})
test('activate leert disabledAt, disabledReason und aboEndeMailAt und laesst zahlungMeldung unveraendert', async () => {
aw.seed(
'portalFeatures',
termineDoc('f_a11_activate', {
state: 'disabled',
enabled: false,
disabledAt: '2026-09-01T10:00:00.000Z',
disabledReason: 'abo_ende',
aboEndeMailAt: '2026-09-01T10:05:00.000Z',
zahlungMeldung: 'gemeldet',
})
)
const on = await sf.activate('f_a11_activate', { by: 'kenso' })
assert.equal(on.state, 'active')
assert.equal(on.enabled, true)
assert.equal(on.disabledAt, null)
assert.equal(on.disabledReason, null)
assert.equal(on.aboEndeMailAt, null)
assert.equal(on.zahlungMeldung, 'gemeldet', 'ein offener Zahlungsfall geht durch eine Aktivierung nicht verloren')
assert.deepEqual(
[lesen('f_a11_activate').disabledAt, lesen('f_a11_activate').disabledReason, lesen('f_a11_activate').aboEndeMailAt, lesen('f_a11_activate').zahlungMeldung],
[null, null, null, 'gemeldet']
)
// Abschalten und Einschalten im Kreis: danach beginnt eine neue Abschaltung mit neuem Grund
const wieder = await sf.disable('f_a11_activate', { reason: 'kunde_geloescht' })
assert.equal(wieder.disabledReason, 'kunde_geloescht')
assert.ok(wieder.disabledAt)
})
test('disableCustomerFeatures reicht den Grund an alle Dokumente durch, ein schon abgeschaltetes behaelt Grund und disabledAt', async () => {
const frueher = '2026-09-01T10:00:00.000Z'
aw.seed('portalFeatures', termineDoc('f_a11_k_aktiv', { customerId: 'c_a11', projectId: 'p_a11_1' }))
aw.seed('portalFeatures', termineDoc('f_a11_k_setup', { customerId: 'c_a11', projectId: 'p_a11_2', state: 'setup', enabled: false }))
aw.seed('portalFeatures', termineDoc('f_a11_k_bestellt', { customerId: 'c_a11', projectId: 'p_a11_3', state: 'ordered', enabled: false }))
aw.seed('portalFeatures', termineDoc('f_a11_k_schonaus', { customerId: 'c_a11', projectId: 'p_a11_4', state: 'disabled', enabled: false, disabledAt: frueher, disabledReason: 'team', adminNote: 'vom Team' }))
aw.seed('portalFeatures', termineDoc('f_a11_k_off', { customerId: 'c_a11', projectId: 'p_a11_5', state: 'off', enabled: false }))
aw.seed('portalFeatures', termineDoc('f_a11_k_news', { customerId: 'c_a11', projectId: 'p_a11_1', featureKey: 'news' }))
const done = await sf.disableCustomerFeatures('c_a11', 'termine', { adminNote: 'Kunde gelöscht', reason: 'kunde_geloescht' })
assert.deepEqual(done.map((d) => d.$id).sort(), ['f_a11_k_aktiv', 'f_a11_k_bestellt', 'f_a11_k_setup'])
for (const id of ['f_a11_k_aktiv', 'f_a11_k_setup', 'f_a11_k_bestellt']) {
const doc = lesen(id)
assert.deepEqual([doc.state, doc.enabled, doc.disabledReason, doc.adminNote], ['disabled', false, 'kunde_geloescht', 'Kunde gelöscht'], id)
assert.ok(doc.disabledAt, id)
}
const schonAus = lesen('f_a11_k_schonaus')
assert.deepEqual([schonAus.disabledReason, schonAus.disabledAt, schonAus.adminNote], ['team', frueher, 'vom Team'])
assert.equal(lesen('f_a11_k_off').state, 'off')
assert.equal(lesen('f_a11_k_news').state, 'active', 'andere Funktion')
// ohne Grund: team; ein unbekannter Grund ist 400 und schreibt nichts
aw.seed('portalFeatures', termineDoc('f_a11_k2', { customerId: 'c_a11b', projectId: 'p_a11_6' }))
await assert.rejects(() => sf.disableCustomerFeatures('c_a11b', 'termine', { reason: 'frist' }), { status: 400 })
assert.equal(lesen('f_a11_k2').state, 'active')
await sf.disableCustomerFeatures('c_a11b', 'termine', {})
assert.equal(lesen('f_a11_k2').disabledReason, 'team')
})
// ------------------------------- markPaid meldet den Fall und merkt ihn im Dokument (B10a) ---
const HINWEIS = /^\d{4}-\d{2}-\d{2}T[\d:.]+Z System: Zahlung eingegangen \(Sitzung cs_[\w]+\), nichts eingerichtet$/
const bezahlDoc = (id, over = {}) =>
termineDoc(id, { projectId: 'p1', stripeSessionId: `cs_${id}`, purchaseStatus: 'ordered', unlockedByPurchase: false, ...over })
const bezahle = (id) => sf.markPaid({ sessionId: `cs_${id}`, invoiceId: `in_${id}`, invoiceUrl: `https://x/inv/${id}` })
const FAELLE = [
['active', { state: 'active', enabled: true }, 'bereits_aktiv'],
['setup_mit_ticket', { state: 'setup', enabled: false, ticketId: 't_vorhanden' }, 'bereits_aktiv'],
['disabled_team', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_abo_ende', { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_geloescht', { state: 'disabled', enabled: false, disabledReason: 'kunde_geloescht', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_altbestand', { state: 'disabled', enabled: false, disabledAt: '2026-10-01T10:00:00.000Z' }, 'abgeschaltet'],
['disabled_vor_einrichtung', { state: 'disabled', enabled: false, disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-01T10:00:00.000Z' }, 'abo_ende'],
]
for (const [name, zustand, fall] of FAELLE) {
test(`markPaid ${name}: Zahlung wird einmalig verbucht, der Fall ${fall} im Dokument vermerkt, nichts eingerichtet`, async () => {
const id = `f_b10a_${name}`
aw.seed('portalFeatures', bezahlDoc(id, zustand))
const ticketsVorher = aw.docs('workorders').size
const davor = lesen(id)
const { doc, created, fall: gemeldet } = await bezahle(id)
assert.equal(created, false)
assert.equal(gemeldet, fall)
assert.equal(doc.$id, id)
const nachher = lesen(id)
assert.equal(nachher.purchaseStatus, 'paid')
assert.equal(nachher.unlockedByPurchase, true)
assert.equal(nachher.stripeInvoiceId, `in_${id}`)
assert.equal(nachher.invoiceUrl, `https://x/inv/${id}`)
assert.equal(nachher.zahlungMeldung, fall)
assert.match(nachher.adminNote, HINWEIS)
assert.ok(nachher.adminNote.includes(`(Sitzung cs_${id})`))
assert.ok(nachher.adminNote.length <= 300)
assert.deepEqual([nachher.state, nachher.enabled, nachher.ticketId, nachher.disabledReason, nachher.disabledAt], [davor.state, davor.enabled, davor.ticketId, davor.disabledReason, davor.disabledAt], 'Zustand, Ticket und Abschaltgrund bleiben')
assert.equal(aw.docs('workorders').size, ticketsVorher, 'kein Ticket')
assert.deepEqual(doc, nachher, 'liefert das gespeicherte Dokument')
})
}
test('markPaid: ein Folgeereignis bucht nichts doppelt, liefert den Fall aber erneut, bis die Meldung als gemeldet vermerkt ist', async () => {
const id = 'f_b10a_wiederholung'
aw.seed('portalFeatures', bezahlDoc(id, { adminNote: 'Notiz vom Team' }))
const erst = await bezahle(id)
assert.equal(erst.fall, 'bereits_aktiv')
const gebucht = lesen(id)
assert.equal(gebucht.adminNote.endsWith(' | Notiz vom Team'), true, 'aeltere Notiz bleibt dahinter erhalten')
assert.match(gebucht.adminNote.split(' | ')[0], HINWEIS)
// Stripe wiederholt, SEPA schickt completed und async_payment_succeeded: nichts wird geschrieben, der Fall bleibt offen
for (let i = 0; i < 2; i += 1) {
const { ergebnis, schreibend } = await schreibzugriffe(() => bezahle(id))
assert.equal(schreibend, 0, 'kein Schreibzugriff beim Folgeereignis')
assert.deepEqual([ergebnis.created, ergebnis.fall], [false, 'bereits_aktiv'])
assert.deepEqual(lesen(id), gebucht, 'adminNote, purchaseStatus und Rechnung unveraendert')
}
// die Meldung ist raus (B10b): der Marker steht auf gemeldet, danach ist der Fall nicht mehr offen
aw.seed('portalFeatures', { ...lesen(id), zahlungMeldung: 'gemeldet' })
const { ergebnis: danach, schreibend } = await schreibzugriffe(() => bezahle(id))
assert.equal(schreibend, 0)
assert.deepEqual([danach.created, danach.fall], [false, null])
assert.equal(lesen(id).purchaseStatus, 'paid', 'purchaseStatus wird nie mit Meldezustaenden ueberladen')
assert.equal(lesen(id).zahlungMeldung, 'gemeldet')
// ein unbekannter Wert im Marker gilt nicht als offener Fall
aw.seed('portalFeatures', { ...lesen(id), zahlungMeldung: 'unsinn' })
assert.equal((await bezahle(id)).fall, null)
})
test('markPaid: die Notiz haelt 300 Zeichen mit dem neuesten Eintrag vorn', async () => {
const id = 'f_b10a_notiz'
aw.seed('portalFeatures', bezahlDoc(id, { adminNote: 'x'.repeat(300) }))
await bezahle(id)
const note = lesen(id).adminNote
assert.equal(note.length, 300)
assert.match(note, /^\d{4}-\d{2}-\d{2}T[\d:.]+Z System: Zahlung eingegangen \(Sitzung cs_f_b10a_notiz\), nichts eingerichtet \| x+$/)
})
test('markPaid: eine normal bezahlte Bestellung (ordered, setup, active) hat zahlungMeldung leer, ihr Folgeereignis liefert keinen Fall', async () => {
aw.seed('websiteProjects', { $id: 'p_b10a_normal', customerId: 'c1', projectName: 'Normal bezahlt' })
await sf.markOrdered({ customerId: 'c1', projectId: 'p_b10a_normal', key: 'termine', sessionId: 'cs_b10a_normal', consent: {} })
const erst = await sf.markPaid({ sessionId: 'cs_b10a_normal', invoiceId: 'in_n', invoiceUrl: 'https://x/n' })
assert.deepEqual([erst.created, erst.fall], [true, null])
assert.equal(erst.doc.state, 'setup')
assert.equal(erst.doc.purchaseStatus, 'paid')
assert.equal(erst.doc.zahlungMeldung, '')
const notiz = erst.doc.adminNote
// Ticket fertig, Funktion aktiv: ein Folgeereignis ist kein Zahlungsfall
const aktiv = await sf.activate(erst.doc.$id, {})
assert.equal(aktiv.state, 'active')
const { ergebnis, schreibend } = await schreibzugriffe(() => sf.markPaid({ sessionId: 'cs_b10a_normal', invoiceId: 'in_n', invoiceUrl: 'https://x/n' }))
assert.equal(schreibend, 0)
assert.deepEqual([ergebnis.created, ergebnis.fall], [false, null])
assert.equal(lesen(erst.doc.$id).zahlungMeldung, '')
assert.equal(lesen(erst.doc.$id).adminNote, notiz)
assert.equal(lesen(erst.doc.$id).purchaseStatus, 'paid')
// auch nach dem Abschalten bleibt es ohne Fall
await sf.disable(erst.doc.$id, { reason: 'abo_ende' })
const spaeter = await sf.markPaid({ sessionId: 'cs_b10a_normal' })
assert.deepEqual([spaeter.created, spaeter.fall], [false, null])
})
test('markPaid: ordered und fortsetzbares setup (ohne Ticket) laufen wie bisher mit created true und ohne Fall', async () => {
const ordered = 'f_b10a_ordered'
aw.seed('portalFeatures', bezahlDoc(ordered, { state: 'ordered', enabled: false }))
const a = await bezahle(ordered)
assert.deepEqual([a.created, a.fall], [true, null])
assert.equal(a.doc.state, 'setup')
assert.ok(a.doc.ticketId)
const fortsetzbar = 'f_b10a_fortsetzbar'
aw.seed('portalFeatures', bezahlDoc(fortsetzbar, { state: 'setup', enabled: false, purchaseStatus: 'paid', unlockedByPurchase: true }))
const b = await bezahle(fortsetzbar)
assert.deepEqual([b.created, b.fall], [true, null])
assert.ok(b.doc.ticketId)
assert.ok(!lesen(fortsetzbar).adminNote, 'nichts verbucht, die Zahlung stand schon')
})
test('markPaid: ein Dokument im Zustand off bleibt unveraendert, ohne Fall (die Uebernahme ist Sache von adoptPaidSession)', async () => {
const id = 'f_b10a_off'
aw.seed('portalFeatures', bezahlDoc(id, { state: 'off', enabled: false, purchaseStatus: 'failed' }))
const davor = lesen(id)
const { ergebnis, schreibend } = await schreibzugriffe(() => bezahle(id))
assert.equal(schreibend, 0)
assert.deepEqual([ergebnis.created, ergebnis.fall], [false, null])
assert.equal(ergebnis.doc.state, 'off')
assert.deepEqual(lesen(id), davor)
})
test('markPaid: ohne Sitzung oder ohne Dokument gibt es kein Dokument, kein created und keinen Fall', async () => {
assert.deepEqual(await sf.markPaid({}), { doc: null, created: false, fall: null })
assert.deepEqual(await sf.markPaid({ sessionId: 'cs_gibts_nicht' }), { doc: null, created: false, fall: null })
})
test('markPaid: gleichzeitige Aufrufe buchen die Zahlung einmal und liefern alle denselben Fall', async () => {
const id = 'f_b10a_parallel'
aw.seed('portalFeatures', bezahlDoc(id, { state: 'disabled', enabled: false, disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-01T10:00:00.000Z' }))
const { ergebnis, schreibend } = await schreibzugriffe(() => Promise.all([1, 2, 3, 4].map(() => bezahle(id))))
assert.equal(schreibend, 1, 'genau ein Schreibzugriff')
assert.deepEqual(ergebnis.map((r) => [r.created, r.fall]), Array(4).fill([false, 'abo_ende']))
assert.equal(lesen(id).adminNote.split('System: Zahlung eingegangen').length - 1, 1, 'ein Eintrag in der Notiz')
assert.equal(lesen(id).zahlungMeldung, 'abo_ende')
})
// ------------------------------------------------ E2: Abschaltgrund und Loeschfrist in der Ansicht ---
const TAG_MS = 24 * 60 * 60 * 1000
test('frist: LOESCHFRIST_TAGE sind 30, loeschzeit ist disabledAt plus genau 30 x 24 h', () => {
assert.equal(LOESCHFRIST_TAGE, 30)
const bis = loeschzeit('2026-10-04T23:30:00.000Z')
assert.ok(bis instanceof Date)
assert.equal(bis.toISOString(), '2026-11-03T23:30:00.000Z')
assert.equal(bis.getTime() - Date.parse('2026-10-04T23:30:00.000Z'), 30 * TAG_MS)
// 24-Stunden-Rechnung, keine Wanduhr: ueber die Zeitumstellung am 25.10. hinweg bleiben es 720 Stunden
const ueberUmstellung = loeschzeit('2026-10-10T12:00:00.000Z')
assert.equal(ueberUmstellung.toISOString(), '2026-11-09T12:00:00.000Z')
assert.equal(ueberUmstellung.getTime() - Date.parse('2026-10-10T12:00:00.000Z'), 720 * 60 * 60 * 1000)
// Appwrite liefert +00:00; ein Date wird nicht veraendert
assert.equal(loeschzeit('2026-10-04T23:30:00.000+00:00').toISOString(), '2026-11-03T23:30:00.000Z')
const eingabe = new Date('2026-10-04T23:30:00.000Z')
assert.equal(loeschzeit(eingabe).toISOString(), '2026-11-03T23:30:00.000Z')
assert.equal(eingabe.toISOString(), '2026-10-04T23:30:00.000Z', 'die Eingabe bleibt unveraendert')
})
test('frist: loeschTag ist der Berliner Kalendertag, nicht der UTC-Tag', () => {
assert.equal(loeschTag('2026-10-04T23:30:00.000Z'), '2026-11-04', 'Berlin 00:30 am 04.11., UTC waere der 03.11.')
assert.equal(loeschTag('2026-10-04T10:00:00.000Z'), '2026-11-03')
// Sommerzeit: 31.07. 22:30 UTC + 30 Tage = 30.08. 22:30 UTC = 31.08. 00:30 Berlin
assert.equal(loeschTag('2026-07-31T22:30:00.000Z'), '2026-08-31')
assert.equal(loeschTag(new Date('2026-10-04T23:30:00.000Z')), '2026-11-04')
})
test('frist: ungueltig, leer oder fehlend ergibt null (kein Wurf)', () => {
for (const wert of [undefined, null, '', 'kein Datum', '2026-13-45T00:00:00Z', new Date('x'), NaN, Infinity, {}, []]) {
assert.equal(loeschzeit(wert), null, `loeschzeit(${String(wert)})`)
assert.equal(loeschTag(wert), null, `loeschTag(${String(wert)})`)
}
})
const E2_ZUSTAENDE = [
// [Name, Dokumentfelder, erwartet { disabledReason, disabledAt, loeschungAm }]
['disabled abo_ende', { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z', loeschungAm: '2026-11-04' }],
['disabled team', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z' },
{ disabledReason: 'team', disabledAt: '2026-10-01T10:00:00.000Z', loeschungAm: '2026-10-31' }],
['disabled abo_ende_vor_einrichtung', { state: 'disabled', enabled: false, disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-02T08:00:00.000Z' },
{ disabledReason: 'abo_ende_vor_einrichtung', disabledAt: '2026-10-02T08:00:00.000Z', loeschungAm: '2026-11-01' }],
['disabled kunde_geloescht', { state: 'disabled', enabled: false, disabledReason: 'kunde_geloescht', disabledAt: '2026-10-03T08:00:00.000Z' },
{ disabledReason: 'kunde_geloescht', disabledAt: '2026-10-03T08:00:00.000Z', loeschungAm: '2026-11-02' }],
['disabled Altbestand ohne Grund', { state: 'disabled', enabled: false, disabledAt: '2026-10-01T10:00:00.000Z' },
{ disabledReason: null, disabledAt: '2026-10-01T10:00:00.000Z', loeschungAm: '2026-10-31' }],
['disabled ohne disabledAt (Altbestand)', { state: 'disabled', enabled: false, disabledReason: 'team' },
{ disabledReason: 'team', disabledAt: null, loeschungAm: null }],
['disabled mit leerem disabledAt', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: '' },
{ disabledReason: 'team', disabledAt: null, loeschungAm: null }],
['disabled mit unlesbarem disabledAt', { state: 'disabled', enabled: false, disabledReason: 'team', disabledAt: 'kein Datum' },
{ disabledReason: 'team', disabledAt: 'kein Datum', loeschungAm: null }],
// Reste aus frueheren Abschaltungen zaehlen in anderen Zustaenden nie
['active mit Resten', { state: 'active', enabled: true, disabledReason: 'abo_ende', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['setup mit Resten', { state: 'setup', enabled: false, disabledReason: 'team', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['ordered mit Resten', { state: 'ordered', enabled: false, disabledReason: 'team', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['off mit Resten', { state: 'off', enabled: false, disabledReason: 'team', disabledAt: '2026-10-04T23:30:00.000Z' },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
['active ohne Reste', { state: 'active', enabled: true }, { disabledReason: null, disabledAt: null, loeschungAm: null }],
['active nach activate() (null-Felder)', { state: 'active', enabled: true, disabledReason: null, disabledAt: null },
{ disabledReason: null, disabledAt: null, loeschungAm: null }],
]
test('featureView: Abschaltgrund, Zeitpunkt und Loeschtag nur im Zustand disabled, sonst alle drei null', async () => {
aw.seed('customers', { $id: 'c_e2', name: 'Kunde E2' })
E2_ZUSTAENDE.forEach(([name, felder], i) => {
aw.seed('websiteProjects', { $id: `p_e2_${i}`, customerId: 'c_e2', projectName: `E2 ${name}` })
aw.seed('portalFeatures', { $id: `f_e2_${i}`, customerId: 'c_e2', projectId: `p_e2_${i}`, featureKey: 'termine', siteKey: 'abcdefghijklmnop', ...felder })
})
aw.seed('websiteProjects', { $id: 'p_e2_ohne', customerId: 'c_e2', projectName: 'E2 ohne Dokument' })
const liste = await sf.listCustomerFeatures('c_e2')
assert.equal(liste.length, E2_ZUSTAENDE.length + 1)
E2_ZUSTAENDE.forEach(([name, , erwartet], i) => {
const view = liste.find((p) => p.projectId === `p_e2_${i}`).features.termine
assert.deepEqual(
{ disabledReason: view.disabledReason, disabledAt: view.disabledAt, loeschungAm: view.loeschungAm },
erwartet,
name
)
})
// kein Dokument = off: alle drei Felder sind da und null; die Felder der anderen Funktion bleiben davon unberuehrt
const ohne = liste.find((p) => p.projectId === 'p_e2_ohne').features
for (const key of Object.keys(ohne)) {
assert.equal(ohne[key].state, 'off', key)
assert.deepEqual([ohne[key].disabledReason, ohne[key].disabledAt, ohne[key].loeschungAm], [null, null, null], key)
}
const erster = liste.find((p) => p.projectId === 'p_e2_0').features
assert.equal(erster.termine.state, 'disabled')
assert.equal(erster.news.state, 'off')
assert.equal(erster.news.loeschungAm, null)
})
test('featureView: nach disable() steht der Grund in der Ansicht und die Frist ist 30 Tage nach disabledAt', async () => {
aw.seed('websiteProjects', { $id: 'p_e2_live', customerId: 'c_e2', projectName: 'E2 live' })
aw.seed('portalFeatures', { $id: 'f_e2_live', customerId: 'c_e2', projectId: 'p_e2_live', featureKey: 'termine', state: 'active', enabled: true, siteKey: 'abcdefghijklmnop' })
const vorher = (await sf.listCustomerFeatures('c_e2')).find((p) => p.projectId === 'p_e2_live').features.termine
assert.deepEqual([vorher.disabledReason, vorher.disabledAt, vorher.loeschungAm], [null, null, null])
const aus = await sf.disable('f_e2_live', { reason: 'abo_ende' })
const view = (await sf.listCustomerFeatures('c_e2')).find((p) => p.projectId === 'p_e2_live').features.termine
assert.deepEqual([view.state, view.disabledReason, view.disabledAt], ['disabled', 'abo_ende', aus.disabledAt])
assert.equal(view.loeschungAm, loeschTag(aus.disabledAt))
assert.match(view.loeschungAm, /^\d{4}-\d\d-\d\d$/)
const wieder = await sf.activate('f_e2_live', {})
assert.equal(wieder.state, 'active')
const danach = (await sf.listCustomerFeatures('c_e2')).find((p) => p.projectId === 'p_e2_live').features.termine
assert.deepEqual([danach.disabledReason, danach.disabledAt, danach.loeschungAm], [null, null, null], 'Wiedereinschalten leert die Anzeige')
})