K02/K03: Hinweis auf das Ende von Analyse und Terminplaner nur für Kunden, die eine von beiden haben

Der Absatz "Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm."
stand bei jeder Kündigung eines Wartungs-Abos. Nach der Entscheidung vom 05.10.2026 steht er nur
noch, wenn der Kunde ein Funktionsdokument (portalFeatures, featureKey analyse oder termine) in
einem Zustand ausser off hat.

- abo.js: hatAboFunktionen(sub) liest über listCustomerFeatureDocs (wie die übrigen Funktions-
  Abfragen) die Dokumente des Kunden des Abos; ein Lesefehler zählt als ja (eine Logzeile, die Mail
  geht in jedem Fall raus). Der Block funktionen in K02 (Kündigung im Stripe-Kundenportal) und K03
  (Kündigungsseite, weiterhin nur beim Abo) ist jetzt isWartung(sub) && hatAboFunktionen(sub);
  beim Hosting-Abo wird gar nicht erst gelesen. Die Vorlagentexte bleiben unverändert.
- Tests: K02 und K03 ohne Dokument, mit aktiver Analyse, Terminplaner in setup/ordered/disabled,
  nur off (und ohne Zustand), News oder Dokument eines anderen Kunden, Lesefehler (Hinweis bleibt,
  Mail geht raus, eine Logzeile) und Hosting-Abo (kein Hinweis, kein Lesen).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 14:50:03 +00:00
1 parent 2c7cd26532
commit 9350fccd77
2 files changed
+164 -39

No files matched your search

+27 -5
View File
@@ -18,7 +18,7 @@ import {
reconcilePendingSwitch,
switchToWartung,
} from '../services/planSwitch.js'
import { handleFeatureCheckoutEvent, isFeatureCheckoutEvent, paidFeatureOrders } from '../services/siteFeatures.js'
import { handleFeatureCheckoutEvent, isFeatureCheckoutEvent, listCustomerFeatureDocs, paidFeatureOrders } from '../services/siteFeatures.js'
import { featureConfig } from '../services/featureRegistry.js'
import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js'
import {
@@ -68,6 +68,8 @@ function minTermEndOf(sub) {
// Alte Dokumente ohne productKey sind Wartungs-Abos (Spec 6)
const productKeyOf = (sub) => sub?.productKey || 'wartung'
const isWartung = (sub) => Boolean(sub) && productKeyOf(sub) === 'wartung'
// Funktionen, die zum Wartungs-Abo gehoeren und mit ihm enden (portalFeatures.featureKey): Website-Analyse und Terminplaner
const ABO_FUNKTIONEN = ['analyse', 'termine']
// {{produkt}} in den Mails: Katalogname ohne "WEBklar " (F33), ohne gefundenes Abo neutral "Abo"
const productLabelOf = (sub) => (sub ? productLabel(productKeyOf(sub)) : 'Abo')
// {{vertragsende_paragraph}}: Paragraph zum Vertragsende in der Fassung des Kunden (v3 § 9, v4 § 11); ohne
@@ -408,6 +410,24 @@ router.post('/switch', requireSession, async (req, res) => {
// Stripe-Webhook
// ---------------------------------------------------------------------------
/**
* Hat der Kunde dieses Abos die Website-Analyse oder den Terminplaner (Funktionsdokument in portalFeatures mit featureKey analyse
* oder termine, Zustand ausser off; ein Dokument ohne Zustand gilt wie off, wie in der Ansicht)? Nur dann gilt der Hinweis in K02 und
* K03 (Block funktionen), dass beide mit dem Wartungs-Abo enden (Entscheidung 2026-10-05). Der Kunde ist der des Abos (sub.customerId,
* wie in customerNameOf); gelesen wird wie sonst auch ueber listCustomerFeatureDocs. Wirft nie: ein Lesefehler zaehlt als ja (der Kunde
* wird eher informiert als nicht), eine Logzeile, die Mail geht in jedem Fall raus.
*/
async function hatAboFunktionen(sub) {
try {
if (!sub?.customerId) throw new Error('Abo ohne Kunden-ID')
const docs = await listCustomerFeatureDocs(sub.customerId)
return docs.some((doc) => ABO_FUNKTIONEN.includes(doc.featureKey) && (doc.state || 'off') !== 'off')
} catch (err) {
console.error(`[abo] Funktionen des Kunden nicht lesbar, der Hinweis auf das Ende von Analyse und Terminplaner bleibt in der Kündigungsbestätigung (Abo ${sub?.$id || '-'}): ${err.message}`)
return true
}
}
/** Anrede in Kundenmails: Name aus den Kundendaten des Portals, sonst der übergebene Ersatz. */
async function customerNameOf(sub, fallback = '') {
const customer = sub?.customerId
@@ -475,8 +495,9 @@ async function sendPortalCancellationConfirmation(sub, createdUnix) {
vertragsende: fmtDate(endsAt),
vertragsende_paragraph: endParagraphOf(sub),
},
// Hinweis auf nicht genutzte Arbeitszeit und auf das Ende von Website-Analyse und Terminplaner nur beim Wartungs-Abo
blocks: { wartung: isWartung(sub), funktionen: isWartung(sub) },
// Hinweis auf nicht genutzte Arbeitszeit nur beim Wartungs-Abo; auf das Ende von Website-Analyse und Terminplaner nur beim
// Wartungs-Abo eines Kunden, der eine von beiden hat (hatAboFunktionen)
blocks: { wartung: isWartung(sub), funktionen: isWartung(sub) && (await hatAboFunktionen(sub)) },
})
await updateDocument(config.collections.subscriptions, sub.$id, {
cancelRequestedAt: receivedAt.toISOString(),
@@ -947,8 +968,9 @@ router.post('/public/cancel', async (req, res) => {
abo: !storage,
// Hinweis auf nicht genutzte Arbeitszeit nur im Wartungs-Abo (Block innerhalb von abo)
wartung: !storage && isWartung(sub),
// Website-Analyse und Terminplaner enden mit dem Wartungs-Abo (Block innerhalb von abo, nie bei der Speicher-Kündigung)
funktionen: !storage && isWartung(sub),
// Website-Analyse und Terminplaner enden mit dem Wartungs-Abo (Block innerhalb von abo, nie bei der Speicher-Kündigung),
// nur fuer einen Kunden, der eine von beiden hat (hatAboFunktionen)
funktionen: !storage && isWartung(sub) && (await hatAboFunktionen(sub)),
automatisch_gekuendigt: !storage && Boolean(effectiveAt) && d.kind !== 'geschaeftsaufgabe',
sonderkuendigung: !storage && Boolean(effectiveAt) && d.kind === 'geschaeftsaufgabe',
ausserordentlich: !storage && !effectiveAt && d.kind === 'ausserordentlich',