Retention: Zustimmungsnachweise der Funktionsbestellung drei Jahre nach Abo-Ende loeschen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 04:38:00 +00:00
1 parent a629b2519b
commit 5b5b4b90eb
4 files changed
+106 -15

No files matched your search

+71
View File
@@ -17,6 +17,7 @@ const aw = await startAppwriteMock({
takeoverCredits: schemaKeys('takeoverCredits'),
newsPosts: schemaKeys('newsPosts'),
portalFeatures: schemaKeys('portalFeatures'),
featureConsents: schemaKeys('featureConsents'),
},
})
const st = await startFakeStripe()
@@ -327,3 +328,73 @@ test('retention: Analyse-Dokument schaltet auch den Sammeldienst ab', async () =
analyseDeps.client = ok
}
})
test('retention_loescht_featureConsents: Zustimmungsnachweise fälliger Kunden werden geplant, gelöscht und berichtet; andere bleiben', async () => {
const consent = (id, customerId, projectId, featureKey) => aw.seed('featureConsents', {
$id: id, customerId, projectId, featureKey, stripeSessionId: `cs_${id}`, acceptedAt: '2022-02-01T10:00:00.000+00:00',
fassungen: '{}', texte: '[]', textHash: 'x', ip: '10.0.0.1', userAgent: 'Browser', konto: 'k@example.com',
})
// fälliger Kunde: Abo endete 2022, zwei Funktionsbestellungen (Terminplaner und News) in zwei Projekten, sonst nichts
sub('fc_sub', 'fc', { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
stripeSub('fc_sub', 'fc')
consent('fc_c1', 'fc', 'fcp1', 'termine')
consent('fc_c2', 'fc', 'fcp2', 'news')
// nicht fälliger Kunde (laufendes Abo) mit Nachweis, und ein Kunde ohne Abo (nur News) mit Nachweis: beide bleiben wie bisher unberührt
sub('fc2_sub', 'fc2', { status: 'active' })
consent('fc2_c1', 'fc2', 'fc2p1', 'termine')
consent('fc3_c1', 'fc3', 'fc3p1', 'news')
aw.seed('newsPosts', { $id: 'fc3_post', customerId: 'fc3', projectId: 'fc3p1', type: 'note', imageId: null, title: 'Hi', text: 'Hallo' })
// die Abfrage nach customerId hat im Schema (scripts/feature-setup.mjs) einen Index
const { FEATURE_CONSENT_INDEXES } = await import('../scripts/feature-setup.mjs')
assert.ok(FEATURE_CONSENT_INDEXES.some((i) => i.attributes.length === 1 && i.attributes[0] === 'customerId'), 'Index auf featureConsents.customerId fehlt')
const plan = await findDueDeletions(JAN_2026)
const eintrag = plan.news.find((n) => n.customerId === 'fc')
assert.ok(eintrag, 'ein fälliger Kunde nur mit Nachweisen (ohne Beiträge und Funktionen) steht im Plan')
assert.deepEqual(eintrag.consentIds.sort(), ['fc_c1', 'fc_c2'])
assert.deepEqual(eintrag.posts, [])
assert.deepEqual(eintrag.featureIds, [])
assert.ok(!plan.news.some((n) => n.customerId === 'fc2' || n.customerId === 'fc3'), 'nicht fälliger Kunde und Kunde ohne Abo bleiben außerhalb')
assert.match(formatReport(plan), /Kunde fc: 0 News-Beitrag\/-Beiträge mit 0 Foto\(s\) zu löschen, 0 Funktion\(en\) abzuschalten, 2 Zustimmungsnachweis\(e\) zu löschen/)
// Probelauf löscht nichts
const probe = await run({ mode: '', now: JAN_2026, log: () => {} })
assert.equal(probe.applied, false)
assert.ok(aw.docs('featureConsents').has('fc_c1') && aw.docs('featureConsents').has('fc_c2'))
const result = await applyDeletions({ ...plan, customers: plan.customers.filter((c) => c.customerId === 'fc'), news: plan.news.filter((n) => n.customerId === 'fc') })
assert.deepEqual(result.failed, [])
assert.equal(result.deleted.featureConsents, 2)
assert.ok(!aw.docs('featureConsents').has('fc_c1') && !aw.docs('featureConsents').has('fc_c2'))
assert.ok(aw.docs('featureConsents').has('fc2_c1') && aw.docs('featureConsents').has('fc3_c1'), 'Nachweise anderer Kunden bleiben')
assert.match(formatReport(plan, result), /Zustimmungsnachweise zu Funktionsbestellungen: 2 gelöscht\./)
// ein zweiter Lauf findet nichts mehr für diesen Kunden
const danach = await findDueDeletions(JAN_2026)
assert.ok(!danach.news.some((n) => n.customerId === 'fc'))
})
test('retention_loescht_featureConsents: Ein Fehler beim Löschen eines Nachweises stoppt den Rest nicht und landet in failed; ältere Pläne ohne consentIds laufen weiter', async () => {
const consent = (id, customerId) => aw.seed('featureConsents', {
$id: id, customerId, projectId: 'fcxp', featureKey: 'termine', stripeSessionId: '', acceptedAt: '2021-02-01T10:00:00.000+00:00',
fassungen: '{}', texte: '[]', textHash: 'x', ip: '', userAgent: '', konto: 'Mitarbeiter: Test',
})
consent('fcx_c1', 'fcx')
consent('fcx_c2', 'fcx')
aw.failNext({ collection: 'featureConsents', method: 'DELETE', status: 500, message: 'kaputt' })
const result = await applyDeletions({ customers: [], takeover: [], news: [{ customerId: 'fcx', posts: [], featureIds: [], consentIds: ['fcx_c1', 'fcx_c2'] }] })
assert.equal(result.failed.length, 1)
assert.equal(result.failed[0].what, 'featureConsents')
assert.equal(result.failed[0].id, 'fcx_c1')
assert.match(result.failed[0].reason, /kaputt/)
assert.equal(result.deleted.featureConsents, 1)
assert.ok(aw.docs('featureConsents').has('fcx_c1') && !aw.docs('featureConsents').has('fcx_c2'))
// der nächste Lauf holt den Rest nach; ein Eintrag, der inzwischen weg ist (404), zählt nicht
const nochmal = await applyDeletions({ customers: [], takeover: [], news: [{ customerId: 'fcx', posts: [], featureIds: [], consentIds: ['fcx_c1', 'fcx_c2'] }] })
assert.deepEqual(nochmal.failed, [])
assert.equal(nochmal.deleted.featureConsents, 1)
// Pläne ohne consentIds (ältere Form) bleiben gültig
const alt = await applyDeletions({ customers: [], takeover: [], news: [{ customerId: 'fcx', posts: [], featureIds: [] }] })
assert.equal(alt.deleted.featureConsents, 0)
assert.deepEqual(alt.failed, [])
})