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:
1 parent
3bac4093af
commit
fbc40252a3
5 files changed
+306
-28
No files matched your search
+102
-19
@@ -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 }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user