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') 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('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) assert.equal(ordered.consentIp, '1.2.3.4') 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 bleibt bei News (TP2): ein termine-Dokument im Zustand setup wird nicht angefasst', async () => { aw.seed('portalFeatures', { $id: 'f_setup_termine', customerId: 'c1', projectId: 'pt2', featureKey: 'termine', state: 'setup', enabled: false, siteKey: 'bbbbbbbbbbbbbbbb', ticketId: 't_closed' }) aw.seed('workorders', { $id: 't_closed', woid: 'WO-9', status: 'Closed' }) const done = await sf.activateClosedTickets({ customerId: 'c1' }) assert.deepEqual(done.map((d) => d.featureKey), []) assert.equal(aw.docs('portalFeatures').get('f_setup_termine').state, 'setup') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { config } = await import('../server/config.js') await deleteDocument(config.collections.portalFeatures, 'f_setup_termine') }) 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') })