Analyse: Freischalten selbstheilend, Sperre, Retention-Abschaltung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f0eee196ed
commit
37203f00c4
6 files changed
+149
-33
No files matched your search
+44
-30
@@ -14,8 +14,8 @@ const SITE_KEY_RE = /^[a-z0-9]{16,40}$/
|
||||
const COLOR_RE = /^#[0-9a-f]{6}$/
|
||||
const DEFAULT_COLOR = '#111111'
|
||||
|
||||
/** Austauschbar fuer Tests: Sammeldienst-Client und Neubau. */
|
||||
export const analyseDeps = { client: analyticsClient, rebuild: triggerRebuild }
|
||||
/** Austauschbar fuer Tests: Sammeldienst-Client, Neubau und Ticket. */
|
||||
export const analyseDeps = { client: analyticsClient, rebuild: triggerRebuild, ticket: createFeatureTicket }
|
||||
|
||||
function httpError(status, message) {
|
||||
const err = new Error(message)
|
||||
@@ -91,24 +91,26 @@ async function rebuildSafe(project) {
|
||||
async function mailSafe(customerId, template, project) {
|
||||
try {
|
||||
const customer = await customerOf(customerId)
|
||||
if (!customer?.email) return
|
||||
if (!customer?.email) return false
|
||||
await sendCustomerMail(customer.email, {
|
||||
template,
|
||||
values: {
|
||||
kunde_name: customer.name || '',
|
||||
projekt: project.projectName || project.subdomain || '',
|
||||
portal_url: `${config.publicBaseUrl}/dashboard.html#news`,
|
||||
portal_url: `${config.publicBaseUrl}/dashboard.html#analyse`,
|
||||
},
|
||||
})
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(`[analyse] ${template} an Kunde ${customerId} fehlgeschlagen:`, err.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function privacyTicket(customerId, project, siteKey) {
|
||||
const name = project.projectName || project.subdomain || ''
|
||||
const domains = analyseDomains(project)
|
||||
return createFeatureTicket({
|
||||
return analyseDeps.ticket({
|
||||
customerId,
|
||||
project,
|
||||
featureKey: FEATURE_KEY,
|
||||
@@ -137,35 +139,47 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
|
||||
const { project } = eligibility
|
||||
|
||||
const existing = await ensureFeatureDoc(customerId, projectId, FEATURE_KEY)
|
||||
if (existing.state === 'active') {
|
||||
// Schon aktiv: nur die Farbe (Banner) aktualisieren, kein zweites Ticket, keine zweite Mail
|
||||
if (existing.accentColor === color) return { doc: existing, changed: false }
|
||||
const doc = await updateDocument(config.collections.portalFeatures, existing.$id, { accentColor: color })
|
||||
await rebuildSafe(project)
|
||||
return { doc, changed: true }
|
||||
if (existing.state === 'disabled' && by === 'customer') {
|
||||
throw httpError(403, 'Analyse wurde gesperrt – bitte melde dich bei uns')
|
||||
}
|
||||
|
||||
const siteKey = SITE_KEY_RE.test(existing.siteKey || '') ? existing.siteKey : newSiteKey()
|
||||
// Erst beim Sammeldienst anmelden: schlaegt das fehl, bleibt das Dokument unveraendert (erneut versuchbar)
|
||||
await analyseDeps.client.putSite(siteKey, { projectId, domains: analyseDomains(project), active: true })
|
||||
const now = new Date().toISOString()
|
||||
let doc = await updateDocument(config.collections.portalFeatures, existing.$id, {
|
||||
...stateFields('active'),
|
||||
siteKey,
|
||||
accentColor: color,
|
||||
avvVersion: consent.avvVersion,
|
||||
avvAcceptedAt: now,
|
||||
avvAcceptedBy: String(by || 'customer').slice(0, 100),
|
||||
activatedAt: now,
|
||||
offSince: '',
|
||||
})
|
||||
if (!doc.privacyTicketId) {
|
||||
const { ticketId } = await privacyTicket(customerId, project, siteKey)
|
||||
let doc = existing
|
||||
let changed = false
|
||||
if (existing.state !== 'active') {
|
||||
const siteKey = SITE_KEY_RE.test(existing.siteKey || '') ? existing.siteKey : newSiteKey()
|
||||
// Erst beim Sammeldienst anmelden: schlaegt das fehl, bleibt das Dokument unveraendert (erneut versuchbar)
|
||||
await analyseDeps.client.putSite(siteKey, { projectId, domains: analyseDomains(project), active: true })
|
||||
const now = new Date().toISOString()
|
||||
doc = await updateDocument(config.collections.portalFeatures, existing.$id, {
|
||||
...stateFields('active'),
|
||||
siteKey,
|
||||
accentColor: color,
|
||||
avvVersion: consent.avvVersion,
|
||||
avvAcceptedAt: now,
|
||||
avvAcceptedBy: String(by || 'customer').slice(0, 100),
|
||||
activatedAt: now,
|
||||
offSince: '',
|
||||
activatedMailAt: '', // jede neue Aktivierung bekommt ihre K18
|
||||
})
|
||||
changed = true
|
||||
} else if (existing.accentColor !== color) {
|
||||
doc = await updateDocument(config.collections.portalFeatures, existing.$id, { accentColor: color })
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Selbstheilend: was von frueheren Versuchen noch fehlt (Ticket, Mail), wird jetzt nachgeholt.
|
||||
// Ticket vor Mail, Marker erst nach Erfolg => je genau ein Ticket und eine K18.
|
||||
const needTicket = !doc.privacyTicketId
|
||||
const needMail = !doc.activatedMailAt
|
||||
if (needTicket) {
|
||||
const { ticketId } = await privacyTicket(customerId, project, doc.siteKey)
|
||||
doc = await updateDocument(config.collections.portalFeatures, doc.$id, { privacyTicketId: ticketId })
|
||||
}
|
||||
await rebuildSafe(project)
|
||||
await mailSafe(customerId, 'K18', project)
|
||||
return { doc, changed: true }
|
||||
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() })
|
||||
}
|
||||
return { doc, changed: changed || needTicket || needMail }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { rm } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { config } from '../config.js'
|
||||
import { deleteDocument, listDocuments, Query } from './appwriteAdmin.js'
|
||||
import { deleteDocument, getDocument, listDocuments, Query } from './appwriteAdmin.js'
|
||||
import { deleteNewsImage } from './newsImages.js'
|
||||
import { disable } from './siteFeatures.js'
|
||||
import { analyseDeps, analyseDomains } from './analyse.js'
|
||||
import { getStripe } from './stripeBilling.js'
|
||||
import { filesOf, STATUS } from './takeoverCredits.js'
|
||||
|
||||
@@ -219,7 +220,18 @@ export async function applyDeletions(plan) {
|
||||
}
|
||||
for (const id of entry.featureIds) {
|
||||
const off = await attempt('portalFeatures', id, async () => {
|
||||
const feature = await getDocument(FEATURES(), id).catch(() => null)
|
||||
await disable(id, { adminNote: 'Abgeschaltet: Aufbewahrungsfrist abgelaufen' })
|
||||
// Analyse: auch den Sammeldienst abschalten (best effort, die Daten loescht dessen 60-Tage-Job)
|
||||
if (feature?.featureKey === 'analyse' && feature.siteKey) {
|
||||
try {
|
||||
// der Sammeldienst verlangt mindestens eine Domain, auch beim Abschalten
|
||||
const project = await getDocument(config.collections.websiteProjects, feature.projectId)
|
||||
await analyseDeps.client.putSite(feature.siteKey, { projectId: feature.projectId, domains: analyseDomains(project), active: false })
|
||||
} catch (err) {
|
||||
console.warn(`[retention] Sammeldienst für Analyse ${id} nicht abgeschaltet:`, err.message)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (off) deleted.features += 1
|
||||
|
||||
Reference in new issue
Block a user