Freischaltung ueber das Register mit Pruefung von Einstellung und Abo

activateClosedTickets oeffnet jetzt alle Funktionen mit ticketFreischaltung im Register
(ticketFreischaltungKeys) statt fest news und fragt unter der Sperre feature-activate:<id>
nach dem frischen Lesen den Haken freischaltbar(doc). Der Standard fuer termine verlangt eine
gespeicherte Einstellung und ein laufendes Wartungs-Abo. false oder Fehler (z. B. Abo nicht
lesbar) lassen das Dokument in setup: Log und genau ein adminNote-Eintrag "Freischaltung
wartet: <Grund>", keine Mail. News bleibt unveraendert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 13:35:57 +00:00
1 parent f4754f573a
commit f57dad73ca
4 files changed
+541 -10

No files matched your search

+23
View File
@@ -25,6 +25,18 @@ async function termineBestellpruefung({ customerId, projectId, feature }) {
return bestellpruefung({ einstellung, fragebogen, feature, mitAbo: false })
}
/**
* Freischaltpruefung des Terminplaners (Spec 4.5: die Aktivierung bleibt gesperrt, bis ein Abo laeuft): die Einrichtung
* ist erst freizugeben, wenn ein Wartungs-Abo laeuft und eine Einstellung gespeichert ist. Fail-closed: ein Lesefehler
* (Abo, Einstellung) wirft, activateClosedTickets haelt das Dokument dann in 'setup'. Der Aufrufer liest das Dokument
* unter der Sperre frisch und uebergibt es; auch das Abo wird hier frisch gelesen, nie aus einem Zwischenspeicher.
*/
async function termineFreischaltbar(doc) {
const [{ ladeEinstellung }, { laeuft }] = await Promise.all([import('./termine/kontext.js'), import('./aboStatus.js')])
if (!(await laeuft(doc.customerId))) return false
return (await ladeEinstellung(doc.$id)) !== null
}
/**
* Platzhalter der Widerrufsbelehrung mit Muster-Widerrufsformular, die die Bestellseite vor dem Knopf zeigt und der
* Zustimmungsnachweis speichert. Block F (Task F7) ersetzt den Haken belehrung durch widerrufsbelehrungFunktion().
@@ -44,6 +56,7 @@ const STANDARD_HAKEN = {
bestellpruefung: termineBestellpruefung,
unterlagenBereit: async () => true,
belehrung: async () => BELEHRUNG_PLATZHALTER,
freischaltbar: termineFreischaltbar,
},
}
@@ -58,9 +71,19 @@ export function featureKeys() {
return Object.keys(config.features)
}
/**
* Schluessel der Funktionen, die nach einem geschlossenen Einrichtungs-Ticket freigeschaltet werden (ticketFreischaltung: true,
* nur genau true), in Registerreihenfolge. config.features wird bei jedem Aufruf gelesen.
*/
export function ticketFreischaltungKeys() {
return featureKeys().filter((key) => config.features[key].ticketFreischaltung === true)
}
/**
* Haken einer Funktion: { verfuegbar?, bestellpruefung?, unterlagenBereit?, belehrung?, freischaltbar? }.
* Wer config.features.<key>.<haken> als Funktion setzt (Tests), ueberschreibt den Standard.
* freischaltbar(doc) => Promise<boolean>: darf das Dokument (Zustand 'setup', Ticket geschlossen) jetzt aktiv werden?
* Nur true gibt frei; false und jeder Fehler halten es in 'setup' (siehe activateClosedTickets).
*/
export function featureHaken(key) {
const eintrag = featureConfig(key)
+49 -3
View File
@@ -4,7 +4,7 @@ import { createDocument, getDocument, listDocuments, updateDocument, Query } fro
import { withLocks } from './keyedLock.js'
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
import { getStripe, retrieveCheckoutSession } from './stripeBilling.js'
import { anleitungsLink, featureConfig, featureKeys, ticketTexte } from './featureRegistry.js'
import { anleitungsLink, featureConfig, featureHaken, featureKeys, ticketFreischaltungKeys, ticketTexte } from './featureRegistry.js'
import { zustimmungFuerSitzung } from './featureConsents.js'
import { laeuft } from './aboStatus.js'
import { createFeatureTicket } from './ticketAdmin.js'
@@ -472,9 +472,50 @@ export async function adminActivate(customerId, projectId, key, { by, adminNote
return activate(doc.$id, { by, adminNote })
}
/** Alle 'setup'-Funktionen (optional nur eines Kunden), deren Ticket 'Closed' ist, werden 'active'. */
/**
* Haken freischaltbar(doc) der Funktion, falls das Register einen hat (nur true gibt frei). Liefert null (frei) oder den Grund,
* warum das Dokument in 'setup' bleibt: false => Voraussetzungen offen, Fehler (z. B. Abo oder Einstellung nicht lesbar) =>
* Pruefung nicht moeglich; der Fehler wird geloggt, nie geworfen (ein Dokument darf die uebrigen nicht aufhalten).
*/
async function freischaltHindernis(doc) {
const { freischaltbar } = featureHaken(doc.featureKey)
if (typeof freischaltbar !== 'function') return null
try {
return (await freischaltbar(doc)) === true ? null : 'Voraussetzungen noch nicht erfüllt'
} catch (err) {
console.error(`[funktionen] Freischaltung ${doc.featureKey} (${doc.$id}) nicht prüfbar:`, err.message)
return 'Prüfung nicht möglich'
}
}
/**
* Eine blockierte Freischaltung einmal in adminNote vermerken ("<Zeit> System: Freischaltung wartet: <Grund>", neuester Eintrag
* vorn). Steht derselbe Eintrag schon vorn, bleibt alles wie es ist: der 5-Minuten-Job schreibt nicht alle 5 Minuten und sendet
* keine Mail (der Hinweis kein_abo kam mit der Zahlung, siehe onFeaturePaid). Ein Schreibfehler wird nur geloggt.
* Aufruf unter der Sperre des Dokuments mit dem frisch gelesenen Dokument.
*/
async function vermerkeWartet(doc, grund, now) {
const eintrag = `System: Freischaltung wartet: ${grund}`
const neuester = String(doc.adminNote || '').split(' | ')[0]
if (neuester.endsWith(eintrag)) return
console.warn(`[funktionen] Freischaltung ${doc.featureKey} (${doc.$id}) wartet: ${grund}`)
try {
await updateDocument(config.collections.portalFeatures, doc.$id, { adminNote: notizVorn(doc.adminNote, `${now.toISOString()} ${eintrag}`) })
} catch (err) {
console.error(`[funktionen] Wartenotiz für ${doc.$id} nicht gespeichert:`, err.message)
}
}
/**
* Alle 'setup'-Funktionen (optional nur eines Kunden) mit ticketFreischaltung im Register (ticketFreischaltungKeys), deren Ticket
* 'Closed' ist, werden 'active' - sofern der Haken freischaltbar der Funktion (Termine: Einstellung gespeichert und Wartungs-Abo
* laeuft) es erlaubt. Das prueft der Job unter der Sperre feature-activate:<id> nach dem frischen Lesen, damit ein Abo, das zwischen
* Listenlesen und Sperre endet, nicht mehr freischaltet. Gesperrte Dokumente bleiben 'setup' (Notiz und Log, keine Mail).
*/
export async function activateClosedTickets({ now = new Date(), customerId = null } = {}) {
const queries = [Query.equal('featureKey', 'news'), Query.equal('state', 'setup'), Query.limit(1000)]
const keys = ticketFreischaltungKeys()
if (keys.length === 0) return []
const queries = [Query.equal('featureKey', keys), Query.equal('state', 'setup'), Query.limit(1000)]
if (customerId) queries.push(Query.equal('customerId', customerId))
const pending = await listDocuments(config.collections.portalFeatures, queries)
const activated = []
@@ -493,6 +534,11 @@ export async function activateClosedTickets({ now = new Date(), customerId = nul
const updated = await withLocks([`feature-activate:${doc.$id}`], async () => {
const fresh = await getDocument(config.collections.portalFeatures, doc.$id)
if (fresh.state !== 'setup') return null
const grund = await freischaltHindernis(fresh)
if (grund) {
await vermerkeWartet(fresh, grund, now)
return null
}
return updateDocument(config.collections.portalFeatures, doc.$id, {
...stateFields('active'),
activatedAt: now.toISOString(),