Analyse: alle Beendigungswege schalten den Messdienst ab (Kunde/Projekt weg, Domain, Löschfrist, Widerruf Wechsel)

Messdienst: POST /a/admin/sites/:key/off setzt active=0 und offSince einmalig; Portal-Client stopSite.
analyse.js: endAnalyse als gemeinsamer Kern, endAnalyseForCustomer, analyseOnProjectChanged,
analyseDisabled (Sperre/Löschfrist) und täglicher Abgleich runAnalyseReconcile (Projekt/Kunde weg,
Domains nachziehen, K18-Wiederholung, Messdienst für off/disabled, fehlende Entfernen-Tickets nachholen).
Entfernen-Ticket je Abschaltung gekennzeichnet (kein Doppelticket, auch wenn das Speichern der ID scheiterte).
Hooks: Kunde löschen, updateWebsiteProject, withdrawOrder (Wechsel), siteFeatures.disable.

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 fbc40252a3
commit 78170567b4
17 files changed
+616 -70

No files matched your search

+57
View File
@@ -15,6 +15,7 @@ const aw = await startAppwriteMock({
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
hourCredits: schemaKeys('hourCredits'),
portalFeatures: [...schemaKeys('portalFeatures'), 'siteKey', 'offSince', 'activatedAt', 'accentColor', 'removalTicketId'],
},
})
const st = await startFakeStripe()
@@ -28,6 +29,7 @@ const { withdrawalDeadline } = await import('../server/services/deadlines.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { analyseDeps } = await import('../server/services/analyse.js')
const app = await startTestApp({ '/api/abo': aboRoutes })
// Mails landen in der Senke statt bei SMTP (F25): K03/K04 an den Kunden, T08/T09 ans Team sind pruefbar
const mails = mailSink()
@@ -396,6 +398,61 @@ test('Widerruf Wechsel in der Frist: Ereignis widerrufen, kein automatischer Pre
assert.doesNotMatch(t09().text, /Abo in Stripe sofort beenden/)
})
// Analyse hat nur mit Wartungs-Abo Bestand: der Widerruf des Wechsels beendet sie (Messdienst aus, K19)
async function withActiveAnalyse(fn) {
const stops = []
const real = analyseDeps.client
analyseDeps.client = { stopSite: async (k) => { stops.push(k) } }
aw.seed('portalFeatures', { $id: 'wf_feat', customerId: 'c1', projectId: 'wf_p', featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'wfsitekey0123456789' })
try {
return await fn(stops)
} finally {
analyseDeps.client = real
}
}
const wechselIn = async (days) => {
const consentAt = daysAgo(days)
await recordContractEvent({
customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung',
details: { from: 'hosting', switchedAt: consentAt, effectiveAt: consentAt, paidAt: consentAt },
consent: { ...consent, consentAt }, status: 'erledigt',
})
}
test('Widerruf Wechsel in der Frist beendet die Analyse des Kunden (stopSite, off, K19); Fehler lässt den Widerruf auf 200', async () => {
await resetSubH({ units: 0 })
await wechselIn(3)
await withActiveAnalyse(async (stops) => {
const res = await withdraw({ order: 'wechsel' })
assert.equal(res.status, 200)
assert.deepEqual(stops, ['wfsitekey0123456789'])
const d = aw.docs('portalFeatures').get('wf_feat')
assert.equal(d.state, 'off')
assert.ok(d.offSince)
assert.ok(bySubject('Deine Website-Analyse wurde beendet'))
})
// wirft der Messdienst, bleibt der Widerruf 200
await resetSubH({ units: 0 })
await wechselIn(3)
await withActiveAnalyse(async () => {
analyseDeps.client = { stopSite: async () => { throw new Error('Messdienst weg') } }
const res = await withdraw({ order: 'wechsel' })
assert.equal(res.status, 200)
assert.equal((await events('wechsel'))[0].status, 'widerrufen')
})
})
test('Widerruf Wechsel nach Fristende oder Zusatzspeicher ändert die Analyse nicht', async () => {
await resetSubH({ units: 0 })
await wechselIn(30)
await withActiveAnalyse(async (stops) => {
assert.equal((await withdraw({ order: 'wechsel' })).status, 200)
assert.equal((await withdraw({ order: 'zusatzspeicher' })).status, 200)
assert.deepEqual(stops, [])
assert.equal(aw.docs('portalFeatures').get('wf_feat').state, 'active')
})
})
test('Widerruf nach Fristende: 200, nichts geändert', async () => {
await resetSubH({ units: 0 })
const consentAt = daysAgo(30)