- markOrdered speichert IP und Browser der Bestellung nicht mehr am Funktionsdokument (nur im Nachweis featureConsents, dort mit Löschfrist); Altwerte werden bei einer neuen Bestellung geleert - Aufbewahrungslauf (retention): consentIp, consentUserAgent, consentText, consentAt werden an allen Funktionsdokumenten eines fälligen Kunden geleert, auch an abgeschalteten; Fehlschlag hält den Kunden wie die übrigen abhängigen Daten - Einmal-Skript scripts/feature-consent-felder-leeren.mjs (Standard Probelauf, --apply leert nur, wo featureConsents die Angabe trägt) - 30-Tage-Reset (jobs.js): auch eine bezahlte Einrichtung mit Grund abo_ende_vor_einrichtung und noch nicht gemeldetem T19 wird nicht zurückgesetzt, solange zahlungMeldung nicht gemeldet ist (die Spur der Erstattung bleibt) - K22: enddatum ist das Abo-Ende (lage.endedAt), sonst der Tag des Abschaltens; loeschdatum bleibt 30 Tage nach dem Abschalten Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
738 lines
42 KiB
JavaScript
738 lines
42 KiB
JavaScript
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'])
|
||
assert.deepEqual(sf.FEATURE_LABELS, {
|
||
news: { name: 'News-Pinnwand', keine: 'keine News-Pinnwand' },
|
||
termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' },
|
||
})
|
||
})
|
||
|
||
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'])
|
||
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'])
|
||
// 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')
|
||
})
|