Funktionen: disable() idempotent, disabledReason, Schema-Skript feature-setup

disable() laeuft unter einer Sperre je Dokument und aendert ein schon
abgeschaltetes Dokument nicht mehr (disabledAt als Beginn der 30-Tage-Frist,
Grund und Notiz bleiben); nur Altbestand ohne disabledAt bekommt dieses Feld.
Neu: DISABLED_REASONS und der Parameter reason (Vorgabe team, unbekannt = 400),
disableCustomerFeatures reicht ihn durch, die Kundenloeschung nutzt
kunde_geloescht. activate() leert zusaetzlich disabledReason und aboEndeMailAt
und laesst zahlungMeldung unberuehrt.

scripts/feature-setup.mjs legt portalFeatures.disabledReason, aboEndeMailAt und
zahlungMeldung idempotent an (--check meldet Luecken mit Exit 1);
schemaKeys('portalFeatures') zieht die Schluessel mit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 11:33:17 +00:00
1 parent 39225594e5
commit aaed273b62
9 files changed
+472 -18

No files matched your search

+38 -9
View File
@@ -13,6 +13,8 @@ export const FEATURE_LABELS = {
termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' },
}
export const STATES = ['off', 'ordered', 'setup', 'active', 'disabled']
/** Warum eine Funktion abgeschaltet ist (portalFeatures.disabledReason); 'team' ist der Vorgabewert (Abschalten im WOMS, Aufbewahrungsfrist). */
export const DISABLED_REASONS = ['team', 'abo_ende', 'abo_ende_vor_einrichtung', 'kunde_geloescht']
export const DISABLED_MESSAGE =
'Die News-Pinnwand wurde für diese Website abgeschaltet. Schreib uns im Chat, wenn du sie wieder möchtest.'
@@ -295,18 +297,44 @@ export async function activate(docId, { by, adminNote } = {}) {
return updateDocument(config.collections.portalFeatures, docId, {
...stateFields('active'),
activatedAt: new Date().toISOString(),
// Eine wieder eingeschaltete Funktion darf nie in den 30-Tage-Loeschlauf des Terminplaners fallen (disabledAt-Marker)
// Eine wieder eingeschaltete Funktion darf nie in den 30-Tage-Loeschlauf des Terminplaners fallen (disabledAt-Marker);
// Grund und Marker der Abo-Ende-Mail gehoeren zur Abschaltung und enden mit ihr. zahlungMeldung bleibt bewusst unberuehrt:
// ein noch offener Zahlungsfall darf durch eine Aktivierung nicht verloren gehen.
disabledAt: null,
disabledReason: null,
aboEndeMailAt: null,
...(note ? { adminNote: note } : {}),
})
}
export async function disable(docId, { by, adminNote } = {}) {
const note = adminNote ?? byNote(by, 'Abgeschaltet')
const doc = await updateDocument(config.collections.portalFeatures, docId, {
...stateFields('disabled'),
disabledAt: new Date().toISOString(),
...(note ? { adminNote: note } : {}),
function assertReason(reason) {
if (!DISABLED_REASONS.includes(reason)) throw httpError(400, 'Unbekannter Abschaltgrund')
}
/**
* Funktion abschalten. Idempotent: steht das Dokument schon auf `disabled`, bleibt alles wie es ist (disabledAt als Beginn der
* 30-Tage-Loeschfrist, Grund, Notiz) und es gibt keinen Schreibzugriff; ein Doppelklick im WOMS, die Kundenloeschung oder ein
* zweiter Lauf verschieben die Frist also nie. Einzige Ausnahme Altbestand: fehlt einem `disabled`-Dokument disabledAt, wird nur
* dieses Feld gesetzt (sonst fiele es nie in die Loeschung). Die Nacharbeit der Analyse (Sammeldienst) laeuft bei jedem Aufruf.
* @param {string} docId
* @param {{ by?: string, adminNote?: string, reason?: 'team'|'abo_ende'|'abo_ende_vor_einrichtung'|'kunde_geloescht' }} [options]
* @throws {Error} 400 bei unbekanntem Grund (ohne Schreibzugriff), 404 bei unbekanntem Dokument
*/
export async function disable(docId, { by, adminNote, reason = 'team' } = {}) {
assertReason(reason)
const doc = await withLocks([`feature-disable:${docId}`], async () => {
const current = await getDocument(config.collections.portalFeatures, docId)
if (current.state === 'disabled') {
if (current.disabledAt) return current
return updateDocument(config.collections.portalFeatures, docId, { disabledAt: new Date().toISOString() })
}
const note = adminNote ?? byNote(by, 'Abgeschaltet')
return updateDocument(config.collections.portalFeatures, docId, {
...stateFields('disabled'),
disabledAt: new Date().toISOString(),
disabledReason: reason,
...(note ? { adminNote: note } : {}),
})
})
if (doc?.featureKey === 'analyse' && doc.siteKey) await stopAnalyseCollecting(doc)
return doc
@@ -318,8 +346,9 @@ export async function disable(docId, { by, adminNote } = {}) {
* abgeschaltet und danach der erste Fehler weitergereicht.
* @returns {Promise<object[]>} die abgeschalteten Dokumente
*/
export async function disableCustomerFeatures(customerId, key, { by, adminNote } = {}) {
export async function disableCustomerFeatures(customerId, key, { by, adminNote, reason = 'team' } = {}) {
assertKey(key)
assertReason(reason)
const docs = (await listCustomerFeatureDocs(customerId)).filter(
(d) => d.featureKey === key && d.state !== 'off' && d.state !== 'disabled'
)
@@ -327,7 +356,7 @@ export async function disableCustomerFeatures(customerId, key, { by, adminNote }
let failure = null
for (const doc of docs) {
try {
disabled.push(await disable(doc.$id, { by, adminNote }))
disabled.push(await disable(doc.$id, { by, adminNote, reason }))
} catch (err) {
failure ||= err
}