Analyse: K18 mit PDFs und Nachweis, K19 neu, Tickets für Einschalten, Ausschalten und Veröffentlichen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-01 18:50:36 +00:00
1 parent 3bac4093af
commit fbc40252a3
5 files changed
+306 -28

No files matched your search

+102 -19
View File
@@ -10,6 +10,9 @@ import { triggerRebuild } from './previewRebuild.js'
import { ensureFeatureDoc, getFeatureDoc, newSiteKey, stateFields, assertOwnProject } from './siteFeatures.js'
import { ACTIVE_STATUSES, getCurrentSubscription } from './stripeBilling.js'
import { createFeatureTicket } from './ticketAdmin.js'
import { berlinDateTime } from './berlinTime.js'
import { legalPaths, legalPdfAttachments } from './analyseLegal.js'
import { privacyTicketDetails, publishTicketDetails, removalTicketDetails, publishReasonText } from './analyseTexts.js'
const FEATURE_KEY = 'analyse'
const SITE_KEY_RE = /^[a-z0-9]{16,40}$/
@@ -20,7 +23,7 @@ export const MISSING_PRIVACY_MESSAGE =
'Für die Zusatzseite fehlen in Deinen Kundendaten Firma oder Name und eine gültige E-Mail-Adresse. Schreib uns im Chat, wir ergänzen sie.'
/** Austauschbar fuer Tests: Sammeldienst-Client, Neubau und Ticket. */
export const analyseDeps = { client: analyticsClient, rebuild: triggerRebuild, ticket: createFeatureTicket, recordConsent }
export const analyseDeps = { client: analyticsClient, rebuild: triggerRebuild, ticket: createFeatureTicket, recordConsent, legalPdfs: legalPdfAttachments }
function httpError(status, message) {
const err = new Error(message)
@@ -123,17 +126,53 @@ export function validateTextform(textform, { today = berlinToday(new Date()), mi
return { date, channel }
}
/** Neubau anstossen; { ok, reason } mit reason kein_repo | preview_aus | fehler. Wirft nie. */
async function rebuildSafe(project) {
if (!project.repoFullName || project.previewMode === 'off') return
if (!project.repoFullName) return { ok: false, reason: 'kein_repo' }
if (project.previewMode === 'off') return { ok: false, reason: 'preview_aus' }
try {
await analyseDeps.rebuild(project)
const result = await analyseDeps.rebuild(project)
if (result?.error) throw new Error(String(result.error))
return { ok: true }
} catch (err) {
// Zustand bleibt gespeichert, der naechste Neubau holt nach
console.warn(`[analyse] Neubau für Projekt ${project.$id} fehlgeschlagen:`, err.message)
return { ok: false, reason: 'fehler' }
}
}
async function mailSafe(customerId, template, project) {
const absolute = (path) => `${config.publicBaseUrl}${path}`
const projectName = (project) => project.projectName || project.subdomain || ''
const WEG = { chat: 'Chat im Kundenportal', email: 'E-Mail' }
function zeitpunkt(iso) {
const at = new Date(iso)
if (Number.isNaN(at.getTime())) return ''
const { date, time } = berlinDateTime(at)
return `${date}, ${time.slice(0, 2)}:${time.slice(2)} Uhr`
}
const germanDate = (iso) => String(iso || '').split('-').reverse().join('.')
/** K18-Werte und -Bloecke aus dem Portal-Dokument (Nachweis der Zustimmung). */
function k18Parts(doc) {
const textform = Boolean(doc.avvConsentDate)
const paths = legalPaths(doc.avvVersion || config.analyse.avvVersion)
return {
values: {
fassung: doc.avvVersion || config.analyse.avvVersion,
zeitpunkt: zeitpunkt(doc.avvAcceptedAt),
person: doc.avvAcceptedBy || '',
datum: germanDate(doc.avvConsentDate),
weg: WEG[doc.avvConsentChannel] || '',
avv_url: absolute(paths.avv),
lb_url: absolute(paths.lb),
},
blocks: { portal_weg: !textform, textform_weg: textform },
}
}
async function mailSafe(customerId, template, project, { values = {}, blocks = {}, attachments = [] } = {}) {
try {
const customer = await customerOf(customerId)
if (!customer?.email) return false
@@ -141,9 +180,12 @@ async function mailSafe(customerId, template, project) {
template,
values: {
kunde_name: customer.name || '',
projekt: project.projectName || project.subdomain || '',
projekt: projectName(project),
portal_url: `${config.publicBaseUrl}/dashboard.html#analyse`,
...values,
},
blocks,
attachments,
})
return true
} catch (err) {
@@ -152,22 +194,41 @@ async function mailSafe(customerId, template, project) {
}
}
function privacyTicket(customerId, project, siteKey) {
const name = project.projectName || project.subdomain || ''
function privacyTicket(customerId, project, siteKey, rebuild) {
const name = projectName(project)
const domains = analyseHosts(project)
const rebuildNote = rebuild && !rebuild.ok ? `${publishReasonText(rebuild.reason)} Dafür gibt es ein eigenes Ticket „Website-Analyse von ${name} veröffentlichen“.` : ''
return analyseDeps.ticket({
customerId,
project,
featureKey: FEATURE_KEY,
siteKey,
topic: `Datenschutzerklärung von ${name} um Hinweis auf Analyse ergänzen`,
details: [
`Funktion: ${FEATURE_KEY}`,
`Projekt: ${name}`,
`Domain(s): ${domains.join(', ')}`,
`Zusatzseite: https://${domains[domains.length - 1]}/datenschutz-analyse.html`,
'Satz + Link in die bestehende Datenschutzerklärung einfügen',
].join('\n'),
details: privacyTicketDetails({ name, domains, rebuildNote }),
})
}
function publishTicket(customerId, project, siteKey, reason) {
const name = projectName(project)
return analyseDeps.ticket({
customerId,
project,
featureKey: FEATURE_KEY,
siteKey,
topic: `Website-Analyse von ${name} veröffentlichen`,
details: publishTicketDetails({ name, domains: analyseHosts(project), reason }),
})
}
function removalTicket(customerId, project, siteKey) {
const name = projectName(project)
return analyseDeps.ticket({
customerId,
project,
featureKey: FEATURE_KEY,
siteKey,
topic: `Website-Analyse von ${name} entfernen`,
details: removalTicketDetails({ name, domains: analyseHosts(project) }),
})
}
@@ -268,13 +329,27 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
// Ticket vor Mail, Marker erst nach Erfolg => je genau ein Ticket und eine K18.
const needTicket = !doc.privacyTicketId
const needMail = !doc.activatedMailAt
// Erst Neubau, dann Tickets: Das Ergebnis steht im Datenschutz-Ticket und entscheidet ueber das Veroeffentlichen-Ticket
const rebuild = changed || needTicket || needMail ? await rebuildSafe(project) : null
if (needTicket) {
const { ticketId } = await privacyTicket(customerId, project, doc.siteKey)
const { ticketId } = await privacyTicket(customerId, project, doc.siteKey, rebuild)
doc = await updateDocument(config.collections.portalFeatures, doc.$id, { privacyTicketId: ticketId })
}
if (changed || needTicket || needMail) await rebuildSafe(project)
if (needMail && (await mailSafe(customerId, 'K18', project))) {
doc = await updateDocument(config.collections.portalFeatures, doc.$id, { activatedMailAt: new Date().toISOString() })
if (rebuild && !rebuild.ok && !doc.publishTicketId) {
const { ticketId } = await publishTicket(customerId, project, doc.siteKey, rebuild.reason)
doc = await updateDocument(config.collections.portalFeatures, doc.$id, { publishTicketId: ticketId })
}
if (needMail) {
// K18 mit beiden PDFs; sind sie nicht lesbar, geht nichts raus und der Marker bleibt leer (spaetere Wiederholung)
let attachments = null
try {
attachments = await analyseDeps.legalPdfs(doc.avvVersion || config.analyse.avvVersion)
} catch (err) {
console.error(`[analyse] K18-Anhänge für Projekt ${projectId} nicht lesbar:`, err.message)
}
if (attachments && (await mailSafe(customerId, 'K18', project, { ...k18Parts(doc), attachments }))) {
doc = await updateDocument(config.collections.portalFeatures, doc.$id, { activatedMailAt: new Date().toISOString() })
}
}
return { doc, changed: changed || needTicket || needMail }
})
@@ -313,7 +388,15 @@ export async function deactivateAnalyse({ customerId, projectId, now = new Date(
console.warn(`[analyse] Historie (aus) für Projekt ${projectId} fehlgeschlagen:`, err.message)
}
await rebuildSafe(project)
return { doc, changed: true }
let result = doc
try {
const { ticketId } = await removalTicket(customerId, project, existing.siteKey || '')
result = await updateDocument(config.collections.portalFeatures, doc.$id, { removalTicketId: ticketId })
} catch (err) {
// Ausschalten darf nie am Ticket scheitern
console.warn(`[analyse] Entfernen-Ticket für Projekt ${projectId} fehlgeschlagen:`, err.message)
}
return { doc: result, changed: true }
})
}