Analyse: Freischalten, Abschalten, Abo-Pruefung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e66f55c2c
commit
f0eee196ed
10 files changed
+540
-32
No files matched your search
@@ -20,6 +20,7 @@ import { purgeExpiredUploads } from './services/takeoverCredits.js'
|
||||
import { purgePendingChatUploads } from './services/chatAttachments.js'
|
||||
import { purgeUnclaimedNewsImages } from './services/newsImages.js'
|
||||
import { runActivation } from './services/siteFeatures.js'
|
||||
import { runAboCheck } from './services/analyse.js'
|
||||
import giteaWebhookRoutes from './routes/webhook/gitea.js'
|
||||
import pushTimeRoutes from './routes/pushTime.js'
|
||||
import adminGiteaRoutes from './routes/admin/gitea.js'
|
||||
@@ -166,6 +167,16 @@ const server = app.listen(config.port, '0.0.0.0', () => {
|
||||
.catch((err) => console.warn('[funktionen] Freischalten fehlgeschlagen:', err.message))
|
||||
setInterval(activateFeatures, 5 * 60 * 1000).unref()
|
||||
|
||||
// Website-Analyse: aktive Analysen ohne laufendes Wartungs-Abo beenden (taeglich, 2 Minuten nach Start beginnend)
|
||||
if (config.analyse.enabled) {
|
||||
const analyseAboCheck = () =>
|
||||
runAboCheck({ now: new Date() })
|
||||
.then((r) => r.deactivated.length && console.log(`[analyse] ${r.deactivated.length} Analyse(n) ohne Abo beendet`))
|
||||
.catch((err) => console.warn('[analyse] Abo-Prüfung fehlgeschlagen:', err.message))
|
||||
setTimeout(analyseAboCheck, 2 * 60 * 1000).unref()
|
||||
setInterval(analyseAboCheck, 24 * 60 * 60 * 1000).unref()
|
||||
}
|
||||
|
||||
// Neue Gitea-Repos erkennen, Zuordnungs-Mail senden, Erinnerungen/Wiederholungen (alle 10 Minuten)
|
||||
const repoAssign = () =>
|
||||
runRepoAssignJob({ now: new Date() }).catch((err) =>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K18 · Analyse aktiv (Website-Analyse)
|
||||
Betreff: Deine Website-Analyse ist aktiv – {{projekt}}
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
|
||||
Platzhalter: {{kunde_name}}, {{projekt}}, {{portal_url}}
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Deine Website-Analyse ist aktiv – {{projekt}}</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Deine Website-Analyse ist eingeschaltet. ͏ ͏ ͏</span>
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
|
||||
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Aktiv</td>
|
||||
</tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">✓ Website-Analyse</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Deine Website-Analyse ist aktiv</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>die Website-Analyse für Deine Website ist eingeschaltet. Besucher sehen jetzt einen Hinweis-Banner, Du findest die Auswertung im Kundenportal. Wir ergänzen Deine Datenschutzerklärung um einen Hinweis auf die Analyse und melden uns, sobald das erledigt ist.</td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Website-Analyse</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{projekt}}</td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Auswertung öffnen →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!--
|
||||
K19 · Analyse beendet (Website-Analyse)
|
||||
Betreff: Deine Website-Analyse wurde beendet – {{projekt}}
|
||||
Absender: „WEBklar“ <Portal-Postfach>
|
||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
|
||||
Platzhalter: {{kunde_name}}, {{projekt}}, {{portal_url}}
|
||||
-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="supported-color-schemes" content="light dark">
|
||||
<title>Deine Website-Analyse wurde beendet – {{projekt}}</title>
|
||||
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
|
||||
<style>
|
||||
a{color:#111111;}
|
||||
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
|
||||
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f1f0ec;">
|
||||
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Deine Website-Analyse wurde beendet. ͏ ͏ ͏</span>
|
||||
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
|
||||
<tr><td align="center" style="padding:32px 12px;">
|
||||
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
|
||||
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
|
||||
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
|
||||
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Beendet</td>
|
||||
</tr></table></td></tr>
|
||||
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">Website-Analyse</td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Deine Website-Analyse wurde beendet</td></tr>
|
||||
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>die Website-Analyse für Deine Website ist beendet, weil Dein Wartungs-Abo beendet wurde oder Du die Analyse ausgeschaltet hast. Der Banner und die Messung sind von Deiner Website entfernt. Die bisher gesammelten Daten werden nach 60 Tagen gelöscht.</td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Website-Analyse</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{projekt}}</td></tr></table></td></tr>
|
||||
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal →</a></td></tr></table></td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
|
||||
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -6,9 +6,8 @@ import { requireAppwriteStaff } from '../../middleware/appwriteStaff.js'
|
||||
import { envKey, encryptEnv, decryptEnv, envNames, validateEnvChange } from '../../services/previewEnv.js'
|
||||
import { listRunLogs, readRunLog, isRunFile } from '../../services/previewLogs.js'
|
||||
import * as runnerClient from '../../services/previewRunnerClient.js'
|
||||
import { runDeploy, fetchPreviewConfig } from '../../services/previewDeploy.js'
|
||||
import { triggerRebuild, previewBranchOf } from '../../services/previewRebuild.js'
|
||||
import { withLocks } from '../../services/keyedLock.js'
|
||||
import { getRepoInfo } from '../../services/giteaAdmin.js'
|
||||
import { repoHasDevScript } from '../../services/previewDevScript.js'
|
||||
|
||||
const router = Router()
|
||||
@@ -64,29 +63,6 @@ async function view(doc) {
|
||||
}
|
||||
}
|
||||
|
||||
async function previewBranchOf(doc) {
|
||||
return doc.previewBranch || (await getRepoInfo(doc.repoFullName).catch(() => null))?.defaultBranch || 'main'
|
||||
}
|
||||
|
||||
// Neubau anstossen (wie beim Bearbeiten/Reaktivieren eines Projekts)
|
||||
async function triggerRebuild(doc) {
|
||||
const repo = doc.repoFullName
|
||||
const branch = await previewBranchOf(doc)
|
||||
const found = await fetchPreviewConfig(repo, branch).catch(() => null)
|
||||
const cfg = {
|
||||
enabled: true,
|
||||
type: 'static',
|
||||
displayName: doc.projectName,
|
||||
...(found || {}),
|
||||
branch,
|
||||
subdomain: doc.subdomain,
|
||||
}
|
||||
const result = await runDeploy(repo, branch, cfg, { project: doc })
|
||||
// runDeploy meldet Runner-Fehler im Ergebnis; Schutzschalter (503) an WOMS durchreichen
|
||||
if (result?.error && /Server ausgelastet/.test(result.error)) throw fail(503, BUSY)
|
||||
return result
|
||||
}
|
||||
|
||||
const wrap = (fn) => async (req, res) => {
|
||||
try {
|
||||
await fn(req, res)
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// Website-Analyse: ein-/ausschalten je Projekt, Wartungs-Abo-Pruefung, Datenschutz-Ticket, Kundenmails
|
||||
import { config } from '../config.js'
|
||||
import { getDocument, listDocuments, updateDocument, Query } from './appwriteAdmin.js'
|
||||
import { analyticsClient } from './analyticsClient.js'
|
||||
import { withLocks } from './keyedLock.js'
|
||||
import { sendCustomerMail } from './mailer.js'
|
||||
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'
|
||||
|
||||
const FEATURE_KEY = 'analyse'
|
||||
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 }
|
||||
|
||||
function httpError(status, message) {
|
||||
const err = new Error(message)
|
||||
err.status = status
|
||||
return err
|
||||
}
|
||||
|
||||
export const ELIGIBILITY_MESSAGES = {
|
||||
nicht_dein_projekt: 'Kein Zugriff auf dieses Projekt',
|
||||
kein_abo: 'Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.',
|
||||
nicht_oeffentlich: 'Die Website-Analyse gibt es nur für öffentliche Websites.',
|
||||
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 }
|
||||
|
||||
/** Nur Wartungs-Abo (kein Hosting) mit laufendem Status. */
|
||||
async function hasWartungsAbo(customerId) {
|
||||
const sub = await getCurrentSubscription(customerId)
|
||||
return Boolean(sub && sub.productKey === 'wartung' && ACTIVE_STATUSES.has(sub.status))
|
||||
}
|
||||
|
||||
async function loadOwnProject(customerId, projectId) {
|
||||
try {
|
||||
return await assertOwnProject(customerId, projectId)
|
||||
} catch (err) {
|
||||
if (err.status === 403) return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {Promise<{ ok: boolean, reason?: string, project?: object }>} */
|
||||
export async function analyseEligibility(customerId, projectId) {
|
||||
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' }
|
||||
if (project.isPublic !== true) return { ok: false, reason: 'nicht_oeffentlich' }
|
||||
if (!project.subdomain) return { ok: false, reason: 'keine_subdomain' }
|
||||
return { ok: true, project }
|
||||
}
|
||||
|
||||
/** `<subdomain>.project.webklar.com` plus Live-Domain (klein, ohne Schema/Pfad), ohne Doppelte. */
|
||||
export function analyseDomains(project) {
|
||||
const domains = []
|
||||
if (project.subdomain) domains.push(`${String(project.subdomain).trim().toLowerCase()}.project.webklar.com`)
|
||||
const live = String(project.liveDomain || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^[a-z]+:\/\//, '')
|
||||
.replace(/[/?#].*$/, '')
|
||||
if (live) domains.push(live)
|
||||
return [...new Set(domains)]
|
||||
}
|
||||
|
||||
async function customerOf(customerId) {
|
||||
try {
|
||||
return await getDocument(config.collections.customers, customerId)
|
||||
} catch (err) {
|
||||
if (err.status === 404) return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildSafe(project) {
|
||||
if (!project.repoFullName || project.previewMode === 'off') return
|
||||
try {
|
||||
await analyseDeps.rebuild(project)
|
||||
} catch (err) {
|
||||
// Zustand bleibt gespeichert, der naechste Neubau holt nach
|
||||
console.warn(`[analyse] Neubau für Projekt ${project.$id} fehlgeschlagen:`, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function mailSafe(customerId, template, project) {
|
||||
try {
|
||||
const customer = await customerOf(customerId)
|
||||
if (!customer?.email) return
|
||||
await sendCustomerMail(customer.email, {
|
||||
template,
|
||||
values: {
|
||||
kunde_name: customer.name || '',
|
||||
projekt: project.projectName || project.subdomain || '',
|
||||
portal_url: `${config.publicBaseUrl}/dashboard.html#news`,
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(`[analyse] ${template} an Kunde ${customerId} fehlgeschlagen:`, err.message)
|
||||
}
|
||||
}
|
||||
|
||||
function privacyTicket(customerId, project, siteKey) {
|
||||
const name = project.projectName || project.subdomain || ''
|
||||
const domains = analyseDomains(project)
|
||||
return createFeatureTicket({
|
||||
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'),
|
||||
})
|
||||
}
|
||||
|
||||
export async function activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer' }) {
|
||||
if (!consent || consent.accepted !== true || consent.avvVersion !== config.analyse.avvVersion) {
|
||||
throw httpError(400, 'Bitte den Auftragsverarbeitungsvertrag bestätigen')
|
||||
}
|
||||
const color = accentColor === undefined || accentColor === null || accentColor === '' ? DEFAULT_COLOR : String(accentColor).toLowerCase()
|
||||
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 { 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 }
|
||||
}
|
||||
|
||||
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)
|
||||
doc = await updateDocument(config.collections.portalFeatures, doc.$id, { privacyTicketId: ticketId })
|
||||
}
|
||||
await rebuildSafe(project)
|
||||
await mailSafe(customerId, 'K18', project)
|
||||
return { doc, changed: true }
|
||||
})
|
||||
}
|
||||
|
||||
export async function deactivateAnalyse({ customerId, projectId, now = new Date() }) {
|
||||
return withLocks([`analyse:${projectId}`], async () => {
|
||||
const project = await loadOwnProject(customerId, projectId)
|
||||
if (!project) throw httpError(403, ELIGIBILITY_MESSAGES.nicht_dein_projekt)
|
||||
const existing = await getFeatureDoc(customerId, projectId, FEATURE_KEY)
|
||||
if (!existing || existing.state === 'off') return { doc: existing, changed: false }
|
||||
if (existing.siteKey) {
|
||||
await analyseDeps.client.putSite(existing.siteKey, { projectId, domains: analyseDomains(project), active: false })
|
||||
}
|
||||
const doc = await updateDocument(config.collections.portalFeatures, existing.$id, {
|
||||
...stateFields('off'),
|
||||
offSince: now.toISOString(),
|
||||
})
|
||||
await rebuildSafe(project)
|
||||
return { doc, changed: true }
|
||||
})
|
||||
}
|
||||
|
||||
/** Taeglich: aktive Analyse ohne laufendes Wartungs-Abo wird beendet, Kunde bekommt K19. */
|
||||
export async function runAboCheck({ now = new Date() } = {}) {
|
||||
const docs = await listDocuments(config.collections.portalFeatures, [
|
||||
Query.equal('featureKey', FEATURE_KEY),
|
||||
Query.equal('state', 'active'),
|
||||
Query.limit(1000),
|
||||
])
|
||||
const deactivated = []
|
||||
for (const doc of docs) {
|
||||
try {
|
||||
// Ein Lesefehler (Stripe/Appwrite) beendet nie eine Analyse
|
||||
if (await hasWartungsAbo(doc.customerId)) continue
|
||||
const { changed } = await deactivateAnalyse({ customerId: doc.customerId, projectId: doc.projectId, now })
|
||||
if (!changed) continue
|
||||
deactivated.push(doc.$id)
|
||||
const project = await getDocument(config.collections.websiteProjects, doc.projectId).catch(() => ({}))
|
||||
await mailSafe(doc.customerId, 'K19', project)
|
||||
} catch (err) {
|
||||
console.error(`[analyse] Abo-Prüfung für Projekt ${doc.projectId} fehlgeschlagen:`, err.message)
|
||||
}
|
||||
}
|
||||
return { checked: docs.length, deactivated }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// Neubau einer Preview/Live-Seite anstossen (WOMS-Preview-Block, Website-Analyse ein/aus)
|
||||
import { getRepoInfo } from './giteaAdmin.js'
|
||||
import { runDeploy, fetchPreviewConfig } from './previewDeploy.js'
|
||||
|
||||
const BUSY = 'Server ausgelastet, versuche es gleich noch mal'
|
||||
|
||||
export async function previewBranchOf(doc) {
|
||||
return doc.previewBranch || (await getRepoInfo(doc.repoFullName).catch(() => null))?.defaultBranch || 'main'
|
||||
}
|
||||
|
||||
// Neubau anstossen (wie beim Bearbeiten/Reaktivieren eines Projekts)
|
||||
export async function triggerRebuild(doc) {
|
||||
const repo = doc.repoFullName
|
||||
const branch = await previewBranchOf(doc)
|
||||
const found = await fetchPreviewConfig(repo, branch).catch(() => null)
|
||||
const cfg = {
|
||||
enabled: true,
|
||||
type: 'static',
|
||||
displayName: doc.projectName,
|
||||
...(found || {}),
|
||||
branch,
|
||||
subdomain: doc.subdomain,
|
||||
}
|
||||
const result = await runDeploy(repo, branch, cfg, { project: doc })
|
||||
// runDeploy meldet Runner-Fehler im Ergebnis; Schutzschalter (503) an WOMS durchreichen
|
||||
if (result?.error && /Server ausgelastet/.test(result.error)) {
|
||||
const err = new Error(BUSY)
|
||||
err.status = 503
|
||||
err.expected = true
|
||||
throw err
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -45,7 +45,7 @@ function assertKey(key) {
|
||||
}
|
||||
|
||||
/** state und enabled werden immer zusammen geschrieben. */
|
||||
function stateFields(state) {
|
||||
export function stateFields(state) {
|
||||
return { state, enabled: state === 'active' }
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function assertOwnProject(customerId, projectId) {
|
||||
}
|
||||
|
||||
/** Dokument holen oder mit state 'off' und dauerhaftem siteKey anlegen. */
|
||||
async function ensureFeatureDoc(customerId, projectId, key) {
|
||||
export async function ensureFeatureDoc(customerId, projectId, key) {
|
||||
const existing = await getFeatureDoc(customerId, projectId, key)
|
||||
if (existing) return existing
|
||||
try {
|
||||
@@ -310,7 +310,7 @@ export async function adminActivate(customerId, projectId, key, { by, adminNote
|
||||
|
||||
/** Alle 'setup'-Funktionen (optional nur eines Kunden), deren Ticket 'Closed' ist, werden 'active'. */
|
||||
export async function activateClosedTickets({ now = new Date(), customerId = null } = {}) {
|
||||
const queries = [Query.equal('state', 'setup'), Query.limit(1000)]
|
||||
const queries = [Query.equal('featureKey', 'news'), Query.equal('state', 'setup'), Query.limit(1000)]
|
||||
if (customerId) queries.push(Query.equal('customerId', customerId))
|
||||
const pending = await listDocuments(config.collections.portalFeatures, queries)
|
||||
const activated = []
|
||||
@@ -343,6 +343,7 @@ export async function findActiveBySiteKey(siteKey) {
|
||||
if (!/^[a-z2-7]{16}$/.test(String(siteKey || ''))) return null
|
||||
const [doc] = await listDocuments(config.collections.portalFeatures, [
|
||||
Query.equal('siteKey', siteKey),
|
||||
Query.equal('featureKey', 'news'),
|
||||
Query.equal('state', 'active'),
|
||||
Query.limit(1),
|
||||
])
|
||||
|
||||
@@ -72,7 +72,7 @@ export async function ensureProjectTicket(project, { repoFullName } = {}) {
|
||||
* Einrichtungs-Ticket fuer eine gekaufte Funktion (z. B. News-Pinnwand) einer Kundenwebsite.
|
||||
* Wird geschlossen, sobald die Funktion eingebaut ist; danach schaltet das Portal sie frei.
|
||||
*/
|
||||
export async function createFeatureTicket({ customerId, project, featureKey, siteKey }) {
|
||||
export async function createFeatureTicket({ customerId, project, featureKey, siteKey, topic = null, details = null }) {
|
||||
const customer = await getDocument(config.collections.customers, customerId).catch(() => null)
|
||||
const woid = await generateNextWoid()
|
||||
const projectName = project?.projectName || project?.subdomain || ''
|
||||
@@ -82,7 +82,7 @@ export async function createFeatureTicket({ customerId, project, featureKey, sit
|
||||
const ticket = await createDocument(
|
||||
config.collections.workorders,
|
||||
{
|
||||
topic: `News-Pinnwand einbauen – ${projectName}`.trim(),
|
||||
topic: topic || `News-Pinnwand einbauen – ${projectName}`.trim(),
|
||||
type: 'Webpage',
|
||||
status: 'Open',
|
||||
priority: 1,
|
||||
@@ -92,7 +92,7 @@ export async function createFeatureTicket({ customerId, project, featureKey, sit
|
||||
customerName: customer?.name || '',
|
||||
customerLocation: customer?.location || '',
|
||||
woid,
|
||||
details: [
|
||||
details: details || [
|
||||
`Funktion: ${featureKey}`,
|
||||
`Projekt: ${projectName}`,
|
||||
`Domain: ${domain}`,
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { test, after, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
|
||||
// Das Setup-Skript laedt config.js: erst nach useTestEnv() importieren, daher die Attributnamen hier fest
|
||||
const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId']
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS],
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
},
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { wartungDoc, hosting2Doc } = await import('./support/fixtures.js')
|
||||
const an = await import('../server/services/analyse.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
|
||||
const mails = mailSink()
|
||||
setMailTransportForTests(mails)
|
||||
after(() => {
|
||||
setMailTransportForTests(null)
|
||||
return Promise.all([aw.close(), st.close()])
|
||||
})
|
||||
|
||||
const puts = []
|
||||
const rebuilds = []
|
||||
an.analyseDeps.client = { putSite: async (siteKey, body) => puts.push({ siteKey, ...body }) }
|
||||
an.analyseDeps.rebuild = async (project) => rebuilds.push(project.$id)
|
||||
|
||||
const consent = () => ({ accepted: true, avvVersion: config.analyse.avvVersion })
|
||||
const analyseTickets = () => [...aw.docs('workorders').values()].filter((t) => String(t.topic).includes('Hinweis auf Analyse'))
|
||||
const doc = (c, p) => sf.getFeatureDoc(c, p, 'analyse')
|
||||
|
||||
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', email: 'eins@example.invalid', location: 'Berlin' })
|
||||
aw.seed('customers', { $id: 'c2', name: 'Kunde Zwei', email: 'zwei@example.invalid' })
|
||||
aw.seed('customers', { $id: 'c3', name: 'Kunde Drei', email: 'drei@example.invalid' })
|
||||
aw.seed('subscriptions', wartungDoc)
|
||||
aw.seed('subscriptions', hosting2Doc)
|
||||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', subdomain: 'baeckerei', liveDomain: 'https://Baeckerei.DE/start', isPublic: true, repoFullName: 'WEBklar/baeckerei' })
|
||||
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Privat', subdomain: 'privat', isPublic: false })
|
||||
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c1', projectName: 'Ohne Sub', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'p4', customerId: 'c2', projectName: 'Hosting-Kunde', subdomain: 'hosting', isPublic: true })
|
||||
aw.seed('websiteProjects', { $id: 'p5', customerId: 'c3', projectName: 'Ohne Abo', subdomain: 'ohneabo', isPublic: true })
|
||||
|
||||
beforeEach(() => {
|
||||
puts.length = 0
|
||||
rebuilds.length = 0
|
||||
mails.sent.length = 0
|
||||
})
|
||||
|
||||
test('Berechtigung: Wartungs-Abo, öffentlich, Subdomain, Eigentum', async () => {
|
||||
assert.equal((await an.analyseEligibility('c1', 'p1')).ok, true)
|
||||
assert.deepEqual(await an.analyseEligibility('c1', 'p2'), { ok: false, reason: 'nicht_oeffentlich' })
|
||||
assert.deepEqual(await an.analyseEligibility('c1', 'p3'), { ok: false, reason: 'keine_subdomain' })
|
||||
assert.deepEqual(await an.analyseEligibility('c2', 'p4'), { ok: false, reason: 'kein_abo' }, 'Hosting-Abo reicht nicht')
|
||||
assert.deepEqual(await an.analyseEligibility('c3', 'p5'), { ok: false, reason: 'kein_abo' })
|
||||
assert.deepEqual(await an.analyseEligibility('c1', 'p4'), { ok: false, reason: 'nicht_dein_projekt' })
|
||||
assert.deepEqual(await an.analyseEligibility('c1', 'gibts-nicht'), { ok: false, reason: 'nicht_dein_projekt' })
|
||||
})
|
||||
|
||||
test('Einschalten abgelehnt: Hosting-Abo, kein Abo, nicht öffentlich, AVV fehlt, Farbe', async () => {
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c2', projectId: 'p4', consent: consent() }), { status: 403 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c3', projectId: 'p5', consent: consent() }), { status: 403 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p2', consent: consent() }), { status: 409 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1' }), { status: 400, message: 'Bitte den Auftragsverarbeitungsvertrag bestätigen' })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: { accepted: true, avvVersion: 'alt' } }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: { accepted: false, avvVersion: config.analyse.avvVersion } }), { status: 400 })
|
||||
await assert.rejects(an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: 'rot', consent: consent() }), { status: 400 })
|
||||
assert.equal(puts.length, 0)
|
||||
assert.equal(analyseTickets().length, 0)
|
||||
})
|
||||
|
||||
test('Einschalten: Dokument, Sammeldienst, Neubau, Ticket, Mail K18', async () => {
|
||||
const { doc: d, changed } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', consent: consent() })
|
||||
assert.equal(changed, true)
|
||||
assert.equal(d.featureKey, 'analyse')
|
||||
assert.equal(d.state, 'active')
|
||||
assert.equal(d.enabled, true)
|
||||
assert.match(d.siteKey, /^[a-z0-9]{16,40}$/)
|
||||
assert.equal(d.accentColor, '#1a73e8')
|
||||
assert.equal(d.avvVersion, config.analyse.avvVersion)
|
||||
assert.equal(d.avvAcceptedBy, 'customer')
|
||||
assert.ok(d.avvAcceptedAt && d.activatedAt)
|
||||
assert.deepEqual(puts, [{ siteKey: d.siteKey, projectId: 'p1', domains: ['baeckerei.project.webklar.com', 'baeckerei.de'], active: true }])
|
||||
assert.deepEqual(rebuilds, ['p1'])
|
||||
const tickets = analyseTickets()
|
||||
assert.equal(tickets.length, 1)
|
||||
assert.equal(tickets[0].topic, 'Datenschutzerklärung von Bäckerei um Hinweis auf Analyse ergänzen')
|
||||
assert.match(tickets[0].details, /Funktion: analyse/)
|
||||
assert.match(tickets[0].details, /Domain\(s\): baeckerei\.project\.webklar\.com, baeckerei\.de/)
|
||||
assert.match(tickets[0].details, /https:\/\/baeckerei\.de\/datenschutz-analyse\.html/)
|
||||
assert.match(tickets[0].details, /Satz \+ Link in die bestehende Datenschutzerklärung einfügen/)
|
||||
assert.equal((await doc('c1', 'p1')).privacyTicketId, tickets[0].$id)
|
||||
assert.equal(mails.sent.length, 1)
|
||||
assert.equal(mails.sent[0].subject, 'Deine Website-Analyse ist aktiv – Bäckerei')
|
||||
assert.deepEqual(mails.sent[0].to.map((t) => t.address), ['eins@example.invalid'])
|
||||
})
|
||||
|
||||
test('Doppelt einschalten: kein zweites Ticket, keine zweite Mail, nur Farbe', async () => {
|
||||
const before = await doc('c1', 'p1')
|
||||
const same = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#1a73e8', consent: consent() })
|
||||
assert.equal(same.changed, false)
|
||||
assert.equal(rebuilds.length, 0)
|
||||
const recolored = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', accentColor: '#CC0000', consent: consent() })
|
||||
assert.equal(recolored.doc.accentColor, '#cc0000')
|
||||
assert.equal(recolored.doc.siteKey, before.siteKey)
|
||||
assert.deepEqual(rebuilds, ['p1'])
|
||||
assert.equal(analyseTickets().length, 1)
|
||||
assert.equal(mails.sent.length, 0)
|
||||
assert.equal(puts.length, 0)
|
||||
})
|
||||
|
||||
test('Ausschalten: off, offSince, Sammeldienst inaktiv, Neubau; erneut = nichts', async () => {
|
||||
const now = new Date('2026-10-05T10:00:00.000Z')
|
||||
const { doc: d, changed } = await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p1', now })
|
||||
assert.equal(changed, true)
|
||||
assert.equal(d.state, 'off')
|
||||
assert.equal(d.enabled, false)
|
||||
assert.equal(d.offSince, '2026-10-05T10:00:00.000Z')
|
||||
assert.equal(puts.length, 1)
|
||||
assert.equal(puts[0].active, false)
|
||||
assert.deepEqual(rebuilds, ['p1'])
|
||||
assert.equal((await an.deactivateAnalyse({ customerId: 'c1', projectId: 'p1', now })).changed, false)
|
||||
await assert.rejects(an.deactivateAnalyse({ customerId: 'c2', projectId: 'p1' }), { status: 403 })
|
||||
})
|
||||
|
||||
test('Wieder einschalten: gleicher siteKey, offSince leer, kein neues Ticket', async () => {
|
||||
const before = await doc('c1', 'p1')
|
||||
const { doc: d } = await an.activateAnalyse({ customerId: 'c1', projectId: 'p1', consent: consent(), by: 'Kenso' })
|
||||
assert.equal(d.siteKey, before.siteKey)
|
||||
assert.equal(d.state, 'active')
|
||||
assert.equal(d.offSince, '')
|
||||
assert.equal(d.accentColor, '#111111')
|
||||
assert.equal(d.avvAcceptedBy, 'Kenso')
|
||||
assert.equal(analyseTickets().length, 1)
|
||||
})
|
||||
|
||||
test('Abo-Ende-Job: aktive Analyse ohne Wartungs-Abo wird beendet, K19', async () => {
|
||||
// c2 hat nur Hosting-Abo, c3 gar keins: beide Analysen von Hand "aktiv" (z. B. Abo wurde gewechselt)
|
||||
for (const [c, p] of [['c2', 'p4'], ['c3', 'p5']]) {
|
||||
const d = await sf.ensureFeatureDoc(c, p, 'analyse')
|
||||
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('active'), accentColor: '#111111' })
|
||||
}
|
||||
const now = new Date('2026-11-01T03:15:00.000Z')
|
||||
const result = await an.runAboCheck({ now })
|
||||
assert.equal(result.checked, 3)
|
||||
assert.equal(result.deactivated.length, 2)
|
||||
assert.equal((await doc('c1', 'p1')).state, 'active', 'Wartungs-Abo bleibt aktiv')
|
||||
for (const [c, p] of [['c2', 'p4'], ['c3', 'p5']]) {
|
||||
const d = await doc(c, p)
|
||||
assert.equal(d.state, 'off')
|
||||
assert.equal(d.offSince, now.toISOString())
|
||||
}
|
||||
assert.deepEqual(mails.sent.map((m) => m.subject).sort(), ['Deine Website-Analyse wurde beendet – Hosting-Kunde', 'Deine Website-Analyse wurde beendet – Ohne Abo'])
|
||||
mails.sent.length = 0
|
||||
assert.equal((await an.runAboCheck({ now })).deactivated.length, 0, 'zweiter Lauf: nichts mehr zu tun')
|
||||
assert.equal(mails.sent.length, 0)
|
||||
})
|
||||
|
||||
test('News-Jobs ignorieren Analyse-Dokumente', async () => {
|
||||
const d = await sf.ensureFeatureDoc('c1', 'p2', 'analyse')
|
||||
aw.seed('workorders', { $id: 't_an', status: 'Closed', woid: '1', topic: 'x' })
|
||||
await updateDocument(config.collections.portalFeatures, d.$id, { ...sf.stateFields('setup'), ticketId: 't_an' })
|
||||
assert.deepEqual(await sf.activateClosedTickets({ customerId: 'c1' }), [])
|
||||
assert.deepEqual(await sf.runActivation({ customerId: 'c1' }), [])
|
||||
assert.equal((await doc('c1', 'p2')).state, 'setup')
|
||||
const active = await doc('c1', 'p1')
|
||||
assert.equal(await sf.findActiveBySiteKey(active.siteKey.slice(0, 16)), null, 'Analyse-siteKey ist kein News-Schlüssel')
|
||||
})
|
||||
@@ -6,7 +6,7 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products.
|
||||
|
||||
const DIR = new URL('../server/mail-templates/', import.meta.url)
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1);
|
||||
// Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden
|
||||
// Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse
|
||||
const EXPECTED = [
|
||||
...Array.from({ length: 11 }, (_, i) => `K${String(i + 1).padStart(2, '0')}`),
|
||||
'K12',
|
||||
@@ -15,6 +15,8 @@ const EXPECTED = [
|
||||
'K15',
|
||||
'K16',
|
||||
'K17',
|
||||
'K18',
|
||||
'K19',
|
||||
...Array.from({ length: 11 }, (_, i) => `T${String(i + 1).padStart(2, '0')}`),
|
||||
'T12',
|
||||
'T13',
|
||||
|
||||
Reference in new issue
Block a user