Kundenansicht P5: Analyse im Abo-Modell (Hosting, Zielgruppe erzwungen, Nachweis, Tagesprüfung)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 16:14:35 +00:00
1 parent 23252df359
commit 57d5db2344
4 files changed
+271 -21

No files matched your search

+20
View File
@@ -1,5 +1,8 @@
import { config } from '../config.js'
import { getDocument } from './appwriteAdmin.js'
import { ACTIVE_STATUSES, getCurrentSubscription } from './stripeBilling.js'
import { listContractEvents, openWithdrawals } from './contractEvents.js'
import { neueAnsichtFuerKunde } from './kundenansicht/schalter.js'
/**
* Abo-Zuordnung der Funktionen (V5, Spec E3/E8): Welches Abo deckt welche Funktion? Ein Lesepfad (lies) fuer alle
@@ -87,3 +90,20 @@ export async function aboLaeuftFuer(customerId, key) {
const l = await lies(customerId, new Date())
return l.laeuft && ENTHALTEN_IN[key].includes(l.produkt)
}
/**
* Gehoert dieser Kunde zum Abo-Modell (sieht er die neue Ansicht)? Fuer Jobs und Dienste ohne Cookie (Tagespruefung der Analyse,
* Team-Aktivierung): liest das Kundendokument (kein Dokument: false) und fragt neueAnsichtFuerKunde({ customerId, email }). Nie
* neueAnsichtFuerSession: eine Sitzung braucht dort auch appwriteUserId, die ein Job nicht hat. Lesefehler werden weitergereicht.
*/
export async function aboModellFuerKunde(customerId) {
if (typeof customerId !== 'string' || customerId === '') return false
let kunde
try {
kunde = await getDocument(config.collections.customers, customerId)
} catch (err) {
if (err.status === 404) return false
throw err
}
return neueAnsichtFuerKunde({ customerId, email: kunde.email })
}
+31 -10
View File
@@ -9,6 +9,8 @@ import { sendCustomerMail } from './mailer.js'
import { triggerRebuild } from './previewRebuild.js'
import { ensureFeatureDoc, getFeatureDoc, newSiteKey, stateFields, assertOwnProject } from './siteFeatures.js'
import { laeuft } from './aboStatus.js'
import { aboDeckt, aboLaeuftFuer, aboModellFuerKunde } from './aboFunktionen.js'
import { consentAboAnalyse } from './featureRegistry.js'
import { createFeatureTicket } from './ticketAdmin.js'
import { berlinDateTime } from './berlinTime.js'
import { legalPaths, legalPdfAttachments } from './analyseLegal.js'
@@ -40,6 +42,10 @@ export const ELIGIBILITY_MESSAGES = {
keine_subdomain: 'Für dieses Projekt ist noch keine Subdomain eingerichtet.',
}
const ELIGIBILITY_STATUS = { nicht_dein_projekt: 403, kein_abo: 403, nicht_oeffentlich: 409, keine_subdomain: 409 }
/** Im Abo-Modell (neue Ansicht) reicht auch ein Hosting-Abo: nur dieser Text weicht ab. */
export const ELIGIBILITY_MESSAGES_ABO = { kein_abo: 'Die Website-Analyse gibt es nur mit einem laufenden Hosting- oder Wartungs-Abo.' }
export const eligibilityMessage = (reason, { aboModell = false } = {}) =>
(aboModell && ELIGIBILITY_MESSAGES_ABO[reason]) || ELIGIBILITY_MESSAGES[reason]
/** Nur Wartungs-Abo (kein Hosting) mit laufendem Status; strenge Regel des Abo-Bausteins: ein Spiegel ohne productKey zaehlt nicht. */
const hasWartungsAbo = (customerId) => laeuft(customerId, { altbestand: false })
@@ -53,11 +59,16 @@ async function loadOwnProject(customerId, projectId) {
}
}
/** @returns {Promise<{ ok: boolean, reason?: string, project?: object }>} */
export async function analyseEligibility(customerId, projectId) {
/**
* @param {{ aboModell?: boolean }} [optionen] aboModell (neue Ansicht): ein laufendes Hosting- oder Wartungs-Abo genuegt (aboDeckt:
* nicht gekuendigt, aktiv; in der Probezeit gilt weiter kein_abo); sonst die strenge Regel "Wartungs-Abo laeuft".
* @returns {Promise<{ ok: boolean, reason?: string, project?: object }>}
*/
export async function analyseEligibility(customerId, projectId, { aboModell = false } = {}) {
const project = await loadOwnProject(customerId, projectId)
if (!project) return { ok: false, reason: 'nicht_dein_projekt' }
if (!(await hasWartungsAbo(customerId))) return { ok: false, reason: 'kein_abo' }
const aboOk = aboModell ? (await aboDeckt(customerId, FEATURE_KEY)).ok === true : await hasWartungsAbo(customerId)
if (!aboOk) return { ok: false, reason: 'kein_abo' }
if (project.isPublic !== true) return { ok: false, reason: 'nicht_oeffentlich' }
if (!project.subdomain) return { ok: false, reason: 'keine_subdomain' }
return { ok: true, project }
@@ -278,12 +289,18 @@ async function sendK18(doc, project) {
return doc
}
export async function activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer', actor, textform, audienceConfirmed, now = new Date() }) {
/**
* Analyse einschalten. aboModell (neue Ansicht, nur durch den Kunden): ein Hosting- oder Wartungs-Abo genuegt, die erwartete Fassung
* ist die der Abo-Erklaerung (consentAboAnalyse().avvVersion). nachweis: async () => void, laeuft im selben try wie recordConsent, nach
* dem Dokument und davor; ein Fehler nimmt alles zurueck (kein "on" ohne Nachweis). Beides nur bei einer neuen Aktivierung.
*/
export async function activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer', actor, textform, audienceConfirmed, now = new Date(), aboModell = false, nachweis = null }) {
const viaCustomer = by === 'customer'
const avvVersion = aboModell ? consentAboAnalyse().avvVersion : config.analyse.avvVersion
let accepted
let proof = { date: '', channel: '' }
if (viaCustomer) {
if (!consent || consent.accepted !== true || consent.avvVersion !== config.analyse.avvVersion) {
if (!consent || consent.accepted !== true || consent.avvVersion !== avvVersion) {
throw httpError(400, 'Bitte den Auftragsverarbeitungsvertrag bestätigen')
}
// Name und E-Mail der handelnden Person (bei Admin-Sitzung der Admin, siehe Route)
@@ -299,8 +316,8 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
if (!COLOR_RE.test(color)) throw httpError(400, 'Ungültige Akzentfarbe')
return withLocks([`analyse:${projectId}`], async () => {
const eligibility = await analyseEligibility(customerId, projectId)
if (!eligibility.ok) throw httpError(ELIGIBILITY_STATUS[eligibility.reason], ELIGIBILITY_MESSAGES[eligibility.reason])
const eligibility = await analyseEligibility(customerId, projectId, { aboModell })
if (!eligibility.ok) throw httpError(ELIGIBILITY_STATUS[eligibility.reason], eligibilityMessage(eligibility.reason, { aboModell }))
const { project } = eligibility
if (!analyseDomains(project).length) throw httpError(409, 'Keine gültige Domain für die Analyse')
@@ -323,14 +340,14 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
projectId,
siteKey,
event: 'on',
avvVersion: config.analyse.avvVersion,
avvVersion,
via: viaCustomer ? 'portal' : 'woms',
acceptedAt: at,
acceptedBy: accepted,
textformDate: proof.date,
textformChannel: proof.channel,
audienceConfirmed: true, // Kunden-Haken enthaelt die Zielgruppen-Aussage; Mitarbeiter bestaetigen sie getrennt
audienceTextVersion: config.analyse.avvVersion,
audienceTextVersion: avvVersion,
}
let updated = false
try {
@@ -353,7 +370,9 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
publishTicketId: '',
})
updated = true
// Nachweis erst nach dem Dokument: scheitert er, wird alles zurueckgenommen (kein "on" ohne aktives Dokument)
// Nachweis der Abo-Erklaerung (featureConsents) und Historie erst nach dem Dokument: scheitert einer, wird alles
// zurueckgenommen (kein "on" ohne Nachweis und ohne aktives Dokument)
await nachweis?.()
await analyseDeps.recordConsent(entry)
} catch (err) {
await analyseDeps.client
@@ -480,6 +499,8 @@ export async function runAboCheck({ now = new Date() } = {}) {
try {
// Ein Lesefehler (Stripe/Appwrite) beendet nie eine Analyse
if (await hasWartungsAbo(doc.customerId)) continue
// Neue Ansicht (Abo-Modell): auch ein laufendes Hosting-Abo haelt die Analyse (gekuendigt zaehlt bis zum Abo-Ende); alle anderen wie bisher
if ((await aboModellFuerKunde(doc.customerId)) && (await aboLaeuftFuer(doc.customerId, FEATURE_KEY))) continue
const { changed } = await deactivateAnalyse({ customerId: doc.customerId, projectId: doc.projectId, now, by: 'system' })
if (!changed) continue
deactivated.push(doc.$id)