Shop J3: Tagesjob und Stundenjob, Löschfristen, Kundenlöschung
- shop/jobs.js: taeglicherJob (Käuferdaten bezahlter Bestellungen und Widerrufe 3 Jahre nach Ende des Bestelljahres anonymisieren, Grenze 1. Januar Berlin; unbezahlte/stornierte Bestellungen nach 30 Tagen löschen; Shops seit 90 Tagen disabled samt Artikeln, Bestellungen, Widerrufen, Bildern und shopConfig löschen, Funktionsdokument bleibt disabled mit Vermerk), seitenweise mit Deckel je Lauf, Schritte und Dokumente einzeln abgesichert; stuendlicherJob räumt nie beanspruchte Bilder auf - shop/loeschen.js: gemeinsamer Baustein zum Löschen aller Daten eines Shops (Tagesjob und Löschfristen) - shop/kundenLoeschung.js + customerAdmin.js: Kundenlöschung schaltet den Webshop ab (kunde_geloescht) und schließt die Shops - retention.js: fällige Kunden verlieren auch ihre Shopdaten (vor den Abo-Dokumenten, bei Fehler held) - server/index.js: Minuten- (75 s), Stunden- (130 s) und Tagesjob (200 s) registriert - Tests: shopJobs.test.js, retention.test.js erweitert Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
73820fb1e6
commit
b8055b336c
8 files changed
+941
-8
No files matched your search
@@ -18,12 +18,17 @@ const aw = await startAppwriteMock({
|
||||
newsPosts: schemaKeys('newsPosts'),
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
featureConsents: schemaKeys('featureConsents'),
|
||||
shopConfig: schemaKeys('shopConfig'),
|
||||
shopProducts: schemaKeys('shopProducts'),
|
||||
shopOrders: schemaKeys('shopOrders'),
|
||||
shopWiderrufe: schemaKeys('shopWiderrufe'),
|
||||
},
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
const uploadDir = mkdtempSync(path.join(os.tmpdir(), 'loeschfristen-'))
|
||||
process.env.TAKEOVER_UPLOAD_DIR = uploadDir
|
||||
process.env.NEWS_UPLOAD_DIR = path.join(uploadDir, 'news')
|
||||
process.env.SHOP_UPLOAD_DIR = path.join(uploadDir, 'shop')
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { findDueDeletions, applyDeletions, formatReport, CONSENT_METADATA_KEYS } = await import('../server/services/retention.js')
|
||||
const { run } = await import('../scripts/loeschfristen.mjs')
|
||||
@@ -580,3 +585,89 @@ test('Nachweis-Liste an der Grenze: findDueDeletions bricht ab, bevor etwas gepl
|
||||
await assert.rejects(findDueDeletions(JAN_2026, { limit: grenze }), /Liste abgeschnitten, Paginierung nötig \(featureConsents/)
|
||||
assert.equal([...aw.docs('featureConsents').values()].filter((c) => c.customerId === 'rs').length, grenze, 'nichts gelöscht')
|
||||
})
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ Webshop (J3)
|
||||
|
||||
const SHOP_KEY = 'abcdefghijklmnop'
|
||||
function seedFaelligMitShop(kunde, siteKey = SHOP_KEY) {
|
||||
sub(`${kunde}_sub`, kunde, { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
|
||||
event(`${kunde}_ev`, kunde, `${kunde}_sub`)
|
||||
stripeSub(`${kunde}_sub`, kunde)
|
||||
const shopId = `f_${kunde}_shop`
|
||||
aw.seed('shopConfig', { $id: shopId, customerId: kunde, projectId: `${kunde}p`, siteKey, geoeffnet: false })
|
||||
aw.seed('shopProducts', { $id: `${kunde}_prod`, shopId, customerId: kunde, aktiv: true, sort: 1, name: 'Tasse', produkt: '{}' })
|
||||
aw.seed('shopOrders', { $id: `${kunde}_ord1`, shopId, customerId: kunde, bestellnummer: '26-A', status: 'versendet', kaeuferEmail: 'k@example.com', bestellung: '{}' })
|
||||
aw.seed('shopOrders', { $id: `${kunde}_ord2`, shopId, customerId: kunde, bestellnummer: '26-B', status: 'storniert', kaeuferEmail: 'k@example.com', bestellung: '{}' })
|
||||
aw.seed('shopWiderrufe', { $id: `${kunde}_wid`, shopId, customerId: kunde, bestellnummer: '26-A', name: 'K', email: 'k@example.com', nachricht: 'x' })
|
||||
const ordner = path.join(uploadDir, 'shop', siteKey)
|
||||
mkdirSync(ordner, { recursive: true })
|
||||
writeFileSync(path.join(ordner, 'bild-l.webp'), 'x')
|
||||
return shopId
|
||||
}
|
||||
|
||||
test('Webshop: fällige Kunden verlieren auch Bestellungen, Artikel, Widerrufe, Bilder und shopConfig; andere Kunden bleiben; Bericht nennt es', async () => {
|
||||
seedFaelligMitShop('sh1')
|
||||
seedFaelligMitShop('sh2', 'bcdefghijklmnopq')
|
||||
// nicht fälliger Kunde (laufendes Abo) mit Shop
|
||||
sub('sh3_sub', 'sh3', { status: 'active' })
|
||||
aw.seed('shopConfig', { $id: 'f_sh3_shop', customerId: 'sh3', projectId: 'sh3p', siteKey: 'cdefghijklmnopqr' })
|
||||
aw.seed('shopOrders', { $id: 'sh3_ord', shopId: 'f_sh3_shop', customerId: 'sh3', bestellnummer: '26-C', status: 'versendet', bestellung: '{}' })
|
||||
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
const eintrag = plan.news.find((n) => n.customerId === 'sh1')
|
||||
assert.deepEqual(eintrag.shops, [{ id: 'f_sh1_shop', siteKey: SHOP_KEY }])
|
||||
assert.ok(!plan.news.some((n) => n.customerId === 'sh3'), 'nicht fälliger Kunde bleibt außerhalb')
|
||||
assert.match(formatReport(plan), /Kunde sh1: .*1 Webshop\(s\) samt Bestellungen, Artikeln, Widerrufen und Bildern zu löschen/)
|
||||
// ältere Einträge ohne Shop tragen den Schlüssel nicht (Plan bleibt für andere Daten gleich)
|
||||
assert.ok(plan.news.filter((n) => !n.shops).every((n) => !('shops' in n)))
|
||||
|
||||
const probe = await run({ mode: '', now: JAN_2026, log: () => {} })
|
||||
assert.equal(probe.applied, false)
|
||||
assert.ok(aw.docs('shopOrders').has('sh1_ord1'))
|
||||
|
||||
const reihenfolge = []
|
||||
const echt = globalThis.fetch
|
||||
globalThis.fetch = async (url, optionen) => {
|
||||
if (optionen?.method === 'DELETE') reihenfolge.push(String(url).match(/collections\/([^/]+)\/documents/)?.[1])
|
||||
return echt(url, optionen)
|
||||
}
|
||||
let result
|
||||
try {
|
||||
result = await applyDeletions({ ...plan, customers: plan.customers.filter((c) => c.customerId === 'sh1'), news: plan.news.filter((n) => n.customerId === 'sh1'), takeover: [] })
|
||||
} finally {
|
||||
globalThis.fetch = echt
|
||||
}
|
||||
assert.deepEqual(result.failed, [])
|
||||
assert.deepEqual(result.held, [])
|
||||
assert.equal(result.deleted.shops, 1)
|
||||
assert.equal(result.deleted.shopDocs, 4)
|
||||
for (const [col, ids] of [['shopOrders', ['sh1_ord1', 'sh1_ord2']], ['shopProducts', ['sh1_prod']], ['shopWiderrufe', ['sh1_wid']], ['shopConfig', ['f_sh1_shop']]]) {
|
||||
for (const docId of ids) assert.ok(!aw.docs(col).has(docId), `${col} ${docId} weg`)
|
||||
}
|
||||
assert.ok(!existsSync(path.join(uploadDir, 'shop', SHOP_KEY)), 'Bilder weg')
|
||||
assert.ok(aw.docs('shopOrders').has('sh2_ord1') && aw.docs('shopConfig').has('f_sh2_shop'), 'anderer fälliger Kunde (nicht im Lauf) bleibt')
|
||||
assert.ok(existsSync(path.join(uploadDir, 'shop', 'bcdefghijklmnopq')))
|
||||
assert.ok(aw.docs('shopOrders').has('sh3_ord') && aw.docs('shopConfig').has('f_sh3_shop'), 'laufender Kunde bleibt')
|
||||
// Reihenfolge: Shopdaten vor den Vertragsereignissen und Abo-Dokumenten
|
||||
assert.ok(reihenfolge.lastIndexOf('shopConfig') < reihenfolge.indexOf('contractEvents'))
|
||||
assert.ok(reihenfolge.indexOf('contractEvents') < reihenfolge.indexOf('subscriptions'))
|
||||
assert.match(formatReport(plan, result), /Webshop: 1 Shop\(s\) gelöscht \(4 Dokument\(e\)/)
|
||||
})
|
||||
|
||||
test('Webshop: scheitert das Löschen der Shopdaten, bleiben die Abo-Dokumente stehen (held) und der nächste Lauf setzt fort', async () => {
|
||||
seedFaelligMitShop('sh4', 'defghijklmnopqrs')
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
aw.failNext({ collection: 'shopOrders', method: 'DELETE', status: 500, message: 'kaputt' })
|
||||
const result = await applyDeletions(planVon(plan, 'sh4'))
|
||||
assert.deepEqual(result.held, ['sh4'])
|
||||
assert.equal(result.failed[0].what, 'shopConfig')
|
||||
assert.ok(aw.docs('subscriptions').has('sh4_sub') && aw.docs('contractEvents').has('sh4_ev'), 'Abo-Daten bleiben')
|
||||
assert.ok(aw.docs('shopConfig').has('f_sh4_shop'), 'shopConfig bleibt, bis alles weg ist')
|
||||
|
||||
const zweiter = await findDueDeletions(JAN_2026)
|
||||
assert.ok(zweiter.customers.some((c) => c.customerId === 'sh4'), 'der Kunde ist weiter fällig')
|
||||
const nochmal = await applyDeletions(planVon(zweiter, 'sh4'))
|
||||
assert.deepEqual(nochmal.failed, [])
|
||||
assert.deepEqual(nochmal.held, [])
|
||||
assert.ok(!aw.docs('shopConfig').has('f_sh4_shop') && !aw.docs('subscriptions').has('sh4_sub'))
|
||||
})
|
||||
Reference in new issue
Block a user