Öffnungszeiten: Daten bei Kundenlöschung entfernen
Kunde löschen schaltet die Funktion zeiten wie den Terminplaner ab (Grund kunde_geloescht) und löscht Zeiten, Erinnerungsmerker und Antwortspeicher sofort; Fehler halten die Kundenlöschung nicht auf. Die Löschfristen löschen die Zeiten vor dem Abschalten und zählen sie im Bericht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
efc6dcf332
commit
9a14e874c0
3 files changed
+282
-3
No files matched your search
@@ -291,6 +291,22 @@ export async function deleteCustomerWithPortalAccess(customerId) {
|
||||
console.error(`[customerAdmin] Terminplaner von Kunde ${customerId} nicht abgeschaltet:`, err.message)
|
||||
}
|
||||
|
||||
// Oeffnungszeiten live: wie der Terminplaner abschalten (die Antwort der Website ist sofort aktiv false: Kunde und Funktion fehlen/sind
|
||||
// disabled), danach Zeiten und Erinnerungsmerker sofort loeschen (Spec 5) und den Speicher der Antworten leeren. Beides fuer sich in
|
||||
// try/catch: ein Fehler haelt weder das Loeschen des Kunden noch den jeweils anderen Schritt auf. Geloggt wird nur die Kunden-ID.
|
||||
try {
|
||||
const { disableCustomerFeatures } = await import('./siteFeatures.js')
|
||||
await disableCustomerFeatures(customerId, 'zeiten', { adminNote: 'Kunde gelöscht', reason: 'kunde_geloescht' })
|
||||
} catch (err) {
|
||||
console.error(`[customerAdmin] Öffnungszeiten von Kunde ${customerId} nicht abgeschaltet:`, err.message)
|
||||
}
|
||||
try {
|
||||
const { loescheZeitenVonKunde } = await import('./zeiten/speicher.js')
|
||||
await loescheZeitenVonKunde(customerId)
|
||||
} catch (err) {
|
||||
console.error(`[customerAdmin] Öffnungszeiten von Kunde ${customerId} nicht gelöscht:`, err.message)
|
||||
}
|
||||
|
||||
// Entwuerfe (Terminplaner im Zustand off: nie bestellt oder nach dem Abo-Ende zurueckgesetzt) gehen sofort mit dem Kunden, Funktionsdokument
|
||||
// und Einstellung. Fehler halten das Loeschen nicht auf (wie oben); ein uebrig gebliebener Entwurf faellt nach 90 Tagen im Tageslauf.
|
||||
try {
|
||||
|
||||
@@ -21,6 +21,10 @@ import { filesOf, STATUS } from './takeoverCredits.js'
|
||||
* - News-Pinnwand: Wird ein Kunde geloescht (Frist wie oben), loeschen wir auch seine News-Beitraege samt Fotos
|
||||
* (newsPosts, Bilddateien) und setzen seine Funktionen (portalFeatures) auf disabled. Der Plan fuehrt sie in
|
||||
* plan.news (nur Kunden mit Beitraegen oder noch nicht abgeschalteten Funktionen), damit plan.customers unveraendert bleibt.
|
||||
* - Oeffnungszeiten live: Zeigt der Plan Funktionen 'zeiten' (plan.news[].zeitenIds, Teilmenge von featureIds), loescht der Lauf vor dem
|
||||
* Abschalten die Zeiten und Erinnerungsmerker des Kunden (loescheZeitenVonKunde, per dynamischem Import wie bei siteFeatures).
|
||||
* Scheitert das, werden diese Funktionen nicht abgeschaltet (solange Daten da sind, soll nichts "aus" aussehen); der Fehler steht unter
|
||||
* "Fehlgeschlagen" im Bericht.
|
||||
* Zeitrechnung in deutscher Zeit: Silvester 23:30 in Berlin ist noch das alte Jahr.
|
||||
* Nicht abgedeckt: Vertragsereignisse ohne customerId bzw. ohne zugehoeriges Abo-Dokument (Waisen) werden
|
||||
* weder gefunden noch geloescht; sie muessen von Hand geprueft werden. Listen werden nicht paginiert: wird eine
|
||||
@@ -117,6 +121,7 @@ export async function findDueDeletions(now = new Date(), { limit = LIST_LIMIT }
|
||||
customerId,
|
||||
posts: posts.map((p) => ({ id: p.$id, projectId: p.projectId, imageId: p.imageId || null })),
|
||||
featureIds: features.map((f) => f.$id),
|
||||
zeitenIds: features.filter((f) => f.featureKey === 'zeiten').map((f) => f.$id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -165,12 +170,12 @@ async function deleteQuietly(collection, id) {
|
||||
* Appwrite-Dokumente. Lehnt Stripe ab (beendete Abos lassen sich oft nicht mehr aendern), steht das Abo in manual;
|
||||
* die Appwrite-Dokumente werden trotzdem geloescht, denn die Frist ist abgelaufen.
|
||||
* @returns {Promise<{ deleted: { subscriptions: number, contractEvents: number, takeover: number, stripeCleared: number,
|
||||
* newsPosts: number, newsImages: number, features: number },
|
||||
* newsPosts: number, newsImages: number, features: number, zeiten: number },
|
||||
* manual: { customerId: string, subscriptionId: string, reason: string }[],
|
||||
* failed: { what: string, id: string, reason: string }[] }>}
|
||||
*/
|
||||
export async function applyDeletions(plan) {
|
||||
const deleted = { subscriptions: 0, contractEvents: 0, takeover: 0, stripeCleared: 0, newsPosts: 0, newsImages: 0, features: 0 }
|
||||
const deleted = { subscriptions: 0, contractEvents: 0, takeover: 0, stripeCleared: 0, newsPosts: 0, newsImages: 0, features: 0, zeiten: 0 }
|
||||
const manual = []
|
||||
const failed = []
|
||||
const clear = Object.fromEntries(CONSENT_METADATA_KEYS.map((key) => [key, '']))
|
||||
@@ -217,7 +222,19 @@ export async function applyDeletions(plan) {
|
||||
}
|
||||
if (await attempt('newsPosts', post.id, () => deleteQuietly(NEWS_POSTS(), post.id))) deleted.newsPosts += 1
|
||||
}
|
||||
// Oeffnungszeiten: erst die Daten (Dokumente, Merker, Speicher), dann das Abschalten; scheitert das Loeschen, bleiben diese Funktionen
|
||||
// unangetastet und der Fehler steht im Bericht (wie beim Bild vor dem News-Beitrag)
|
||||
const zeitenIds = new Set(entry.zeitenIds || [])
|
||||
let zeitenWeg = true
|
||||
if (zeitenIds.size) {
|
||||
zeitenWeg = await attempt('oeffnungszeiten', entry.customerId, async () => {
|
||||
const { loescheZeitenVonKunde } = await import('./zeiten/speicher.js')
|
||||
deleted.zeiten += await loescheZeitenVonKunde(entry.customerId)
|
||||
return true
|
||||
})
|
||||
}
|
||||
for (const id of entry.featureIds) {
|
||||
if (!zeitenWeg && zeitenIds.has(id)) continue
|
||||
const off = await attempt('portalFeatures', id, async () => {
|
||||
// disable() meldet bei Analyse auch den Sammeldienst ab (best effort)
|
||||
await disable(id, { adminNote: 'Abgeschaltet: Aufbewahrungsfrist abgelaufen' })
|
||||
@@ -251,7 +268,8 @@ export function formatReport(plan, result = null) {
|
||||
}
|
||||
for (const n of plan.news || []) {
|
||||
const images = n.posts.filter((p) => p.imageId).length
|
||||
lines.push(`Kunde ${n.customerId}: ${n.posts.length} News-Beitrag/-Beiträge mit ${images} Foto(s) zu löschen, ${n.featureIds.length} Funktion(en) abzuschalten`)
|
||||
const zeiten = n.zeitenIds?.length ? `; Öffnungszeiten-Daten von ${n.zeitenIds.length} Funktion(en) zu löschen` : ''
|
||||
lines.push(`Kunde ${n.customerId}: ${n.posts.length} News-Beitrag/-Beiträge mit ${images} Foto(s) zu löschen, ${n.featureIds.length} Funktion(en) abzuschalten${zeiten}`)
|
||||
}
|
||||
const del = plan.takeover.filter((t) => t.action === 'loeschen')
|
||||
const report = plan.takeover.filter((t) => t.action === 'melden')
|
||||
@@ -270,6 +288,7 @@ export function formatReport(plan, result = null) {
|
||||
`Gelöscht: ${d.subscriptions} Abo(s), ${d.contractEvents} Vertragsereignis(se), ${d.takeover} Übernahme-Antrag/-Anträge; Stripe-Metadaten geleert bei ${d.stripeCleared} Abo(s).`,
|
||||
`News: ${d.newsPosts} Beitrag/Beiträge und ${d.newsImages} Foto(s) gelöscht, ${d.features} Funktion(en) abgeschaltet.`
|
||||
)
|
||||
if (d.zeiten) lines.push(`Öffnungszeiten: ${d.zeiten} Datensatz/Datensätze gelöscht.`)
|
||||
if (result.failed?.length) {
|
||||
lines.push('', 'Fehlgeschlagen (Lauf erneut starten):', ...result.failed.map((f) => ` ${f.what} ${f.id}: ${f.reason}`))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { test, after, beforeEach, afterEach, mock } 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 { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
|
||||
// Oeffnungszeiten live, Kunde geloescht (Spec 5): deleteCustomerWithPortalAccess schaltet 'zeiten' wie 'termine' ab (Notiz "Kunde gelöscht",
|
||||
// Grund kunde_geloescht) und loescht Daten und Erinnerungsmerker sofort; die Loeschfristen (retention.js) loeschen die Daten ebenfalls,
|
||||
// bevor sie die Funktion eines faelligen Kunden abschalten. Fehler halten die Kundenloeschung nie auf.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
|
||||
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
contractEvents: schemaKeys('contractEvents'),
|
||||
takeoverCredits: schemaKeys('takeoverCredits'),
|
||||
newsPosts: schemaKeys('newsPosts'),
|
||||
termineConfig: schemaKeys('termineConfig'),
|
||||
},
|
||||
datetimes: ZEITEN_DATETIMES,
|
||||
defaultLimit: 25,
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
|
||||
const { config } = await import('../server/config.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { deleteCustomerWithPortalAccess } = await import('../server/services/customerAdmin.js')
|
||||
const { speichereZeiten } = await import('../server/services/zeiten/speicher.js')
|
||||
const { oeffentlicheZeitenAntwort } = await import('../server/services/zeiten/oeffentlich.js')
|
||||
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
|
||||
const { findDueDeletions, applyDeletions, formatReport } = await import('../server/services/retention.js')
|
||||
|
||||
const JETZT = new Date('2026-10-05T10:00:00.000Z') // Berlin: Montag 2026-10-05 12:00
|
||||
const JAN_2026 = new Date('2026-01-01T12:00:00+01:00')
|
||||
const MODUS_VORHER = config.zeiten.mode
|
||||
|
||||
const tage = (woche = {}) => {
|
||||
const t = {}
|
||||
for (let d = 1; d <= 7; d += 1) t[d] = woche[d] ? { modus: 'zeiten', fenster: [woche[d]] } : { modus: 'zu', fenster: [] }
|
||||
return t
|
||||
}
|
||||
const eingabe = () => ({
|
||||
land: 'RP',
|
||||
plaene: [{ id: 'p_aaaaaaa1', kurz: 'standard', name: 'Standard', haupt: true, tage: tage({ 1: ['09:00', '18:00'] }) }],
|
||||
feiertage: {},
|
||||
ausnahmen: [],
|
||||
hinweise: [],
|
||||
kontakt: { an: false, telefon: '', adresse: '', whatsapp: null },
|
||||
})
|
||||
|
||||
let nr = 0
|
||||
/**
|
||||
* Kunde mit Projekten und je einer Zeiten-Funktion (state), gespeicherten Zeiten und Erinnerungsmerker; mit news: eine aktive News-Funktion.
|
||||
* Liefert { customerId, features: [{ $id, projectId, siteKey, ... }] }.
|
||||
*/
|
||||
async function legeKunde({ projekte = 1, state = 'active', news = false } = {}) {
|
||||
nr += 1
|
||||
const customerId = `zl${nr}`
|
||||
aw.seed('customers', { $id: customerId, name: `Salon ${nr}`, companyName: `Salon ${nr} GmbH`, email: `salon${nr}@example.invalid` })
|
||||
const features = []
|
||||
for (let i = 1; i <= projekte; i += 1) {
|
||||
const projectId = `zlp${nr}_${i}`
|
||||
// eindeutiger siteKey je Funktion: 16 Zeichen aus [a-z2-7]
|
||||
const siteKey = `zeitenkey${'abcdefghijklmnop'[nr]}${'abcdefghijklmnop'[i]}aaaaa`
|
||||
const feature = { $id: `f_${projectId}_zeiten`, customerId, projectId, featureKey: 'zeiten', state, enabled: state === 'active', siteKey }
|
||||
aw.seed('websiteProjects', { $id: projectId, customerId, projectName: `Projekt ${nr}-${i}` })
|
||||
aw.seed('portalFeatures', feature)
|
||||
await speichereZeiten({ feature, eingabe: eingabe(), version: 0, by: 'kunde', now: JETZT })
|
||||
aw.seed('zeitenErinnerungen', { $id: `m_${feature.$id}`, featureId: feature.$id, datum: '2026-12-24', createdAt: '2026-12-01T08:00:00.000Z' })
|
||||
features.push(feature)
|
||||
}
|
||||
if (news) aw.seed('portalFeatures', { $id: `f_${features[0].projectId}_news`, customerId, projectId: features[0].projectId, featureKey: 'news', state: 'active', enabled: true })
|
||||
return { customerId, features }
|
||||
}
|
||||
|
||||
const zeitenDocs = (customerId) => [...aw.docs('oeffnungszeiten').values()].filter((d) => d.customerId === customerId)
|
||||
const merkerVon = (features) => [...aw.docs('zeitenErinnerungen').values()].filter((m) => features.some((f) => f.$id === m.featureId))
|
||||
const featureDoc = (id) => aw.docs('portalFeatures').get(id)
|
||||
|
||||
beforeEach(() => {
|
||||
config.zeiten.mode = 'on'
|
||||
resetZeitenCacheForTests()
|
||||
})
|
||||
afterEach(() => {
|
||||
config.zeiten.mode = MODUS_VORHER
|
||||
mock.restoreAll()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- deleteCustomerWithPortalAccess
|
||||
|
||||
test('Kundenlöschung schaltet zeiten ab und löscht Daten und Merker', async () => {
|
||||
const kunde = await legeKunde({ projekte: 2, news: true })
|
||||
const fremd = await legeKunde()
|
||||
const schonAus = await legeKunde({ state: 'disabled' })
|
||||
const frueher = '2026-09-01T10:00:00.000Z'
|
||||
aw.seed('portalFeatures', { ...featureDoc(schonAus.features[0].$id), disabledAt: frueher, disabledReason: 'abo_ende' })
|
||||
assert.equal(zeitenDocs(kunde.customerId).length, 2)
|
||||
assert.equal(merkerVon(kunde.features).length, 2)
|
||||
|
||||
await deleteCustomerWithPortalAccess(kunde.customerId)
|
||||
await deleteCustomerWithPortalAccess(schonAus.customerId)
|
||||
|
||||
assert.equal(aw.docs('customers').has(kunde.customerId), false)
|
||||
for (const f of kunde.features) {
|
||||
const doc = featureDoc(f.$id)
|
||||
assert.deepEqual([doc.state, doc.enabled, doc.adminNote, doc.disabledReason], ['disabled', false, 'Kunde gelöscht', 'kunde_geloescht'], f.projectId)
|
||||
assert.ok(Math.abs(Date.parse(doc.disabledAt) - Date.now()) < 5000, f.projectId)
|
||||
}
|
||||
assert.deepEqual(zeitenDocs(kunde.customerId), [], 'Zeiten des Kunden gelöscht')
|
||||
assert.deepEqual(merkerVon(kunde.features), [], 'Erinnerungsmerker des Kunden gelöscht')
|
||||
assert.equal(featureDoc(`f_${kunde.features[0].projectId}_news`).state, 'active', 'News ist nicht Sache der Zeiten')
|
||||
|
||||
// ein schon abgeschaltetes Dokument behält den Beginn seiner Frist, seine Daten gehen trotzdem mit dem Kunden
|
||||
assert.equal(featureDoc(schonAus.features[0].$id).disabledAt, frueher)
|
||||
assert.equal(featureDoc(schonAus.features[0].$id).disabledReason, 'abo_ende')
|
||||
assert.deepEqual(zeitenDocs(schonAus.customerId), [])
|
||||
|
||||
// ein anderer Kunde bleibt unberührt
|
||||
assert.equal(featureDoc(fremd.features[0].$id).state, 'active')
|
||||
assert.equal(zeitenDocs(fremd.customerId).length, 1)
|
||||
assert.equal(merkerVon(fremd.features).length, 1)
|
||||
})
|
||||
|
||||
test('öffentliche Antwort danach aktiv false', async () => {
|
||||
const kunde = await legeKunde()
|
||||
const fremd = await legeKunde()
|
||||
const [eigene] = kunde.features
|
||||
assert.equal((await oeffentlicheZeitenAntwort(eigene.siteKey, JETZT)).aktiv, true, 'vorher beantwortet (und im Speicher)')
|
||||
assert.equal((await oeffentlicheZeitenAntwort(fremd.features[0].siteKey, JETZT)).aktiv, true)
|
||||
|
||||
await deleteCustomerWithPortalAccess(kunde.customerId)
|
||||
|
||||
assert.deepEqual(await oeffentlicheZeitenAntwort(eigene.siteKey, JETZT), { aktiv: false }, 'Speicher geleert, Kunde und Funktion weg')
|
||||
assert.deepEqual(await oeffentlicheZeitenAntwort(eigene.siteKey, new Date(JETZT.getTime() + 60000)), { aktiv: false })
|
||||
assert.equal((await oeffentlicheZeitenAntwort(fremd.features[0].siteKey, JETZT)).aktiv, true, 'ein anderer Kunde antwortet weiter')
|
||||
})
|
||||
|
||||
test('Fehler beim Löschen der Zeiten bricht die Kundenlöschung nicht ab', async (t) => {
|
||||
const log = t.mock.method(console, 'error', () => {})
|
||||
const kunde = await legeKunde({ projekte: 2 })
|
||||
aw.failNext({ collection: 'oeffnungszeiten', method: 'DELETE', status: 500, message: 'kaputt' })
|
||||
|
||||
await deleteCustomerWithPortalAccess(kunde.customerId)
|
||||
|
||||
assert.equal(aw.docs('customers').has(kunde.customerId), false, 'der Kunde ist gelöscht')
|
||||
const zeilen = log.mock.calls.map((c) => c.arguments.map(String).join(' '))
|
||||
const zeile = zeilen.find((z) => /Öffnungszeiten von Kunde .* nicht gelöscht/.test(z))
|
||||
assert.ok(zeile, 'Logzeile')
|
||||
assert.ok(zeile.includes(kunde.customerId) && zeile.includes('kaputt'))
|
||||
assert.ok(!zeile.includes('Salon') && !zeile.includes('example.invalid'), 'keine Kundendaten im Log')
|
||||
for (const f of kunde.features) assert.equal(featureDoc(f.$id).state, 'disabled', 'abgeschaltet wurde trotzdem')
|
||||
assert.equal(zeitenDocs(kunde.customerId).length, 1, 'ein Dokument mit Fehler bleibt, das andere ist weg')
|
||||
assert.equal(await oeffentlicheZeitenAntwort(kunde.features[0].siteKey, JETZT).then((a) => a.aktiv), false)
|
||||
})
|
||||
|
||||
test('Fehler beim Abschalten der Zeiten hält weder das Löschen der Daten noch die Kundenlöschung auf', async (t) => {
|
||||
const log = t.mock.method(console, 'error', () => {})
|
||||
const kunde = await legeKunde()
|
||||
aw.failNext({ collection: 'portalFeatures', method: 'PATCH', status: 500, message: 'kaputt' })
|
||||
|
||||
await deleteCustomerWithPortalAccess(kunde.customerId)
|
||||
|
||||
assert.equal(aw.docs('customers').has(kunde.customerId), false)
|
||||
assert.ok(log.mock.calls.some((c) => /Öffnungszeiten von Kunde .* nicht abgeschaltet/.test(String(c.arguments[0]))))
|
||||
assert.deepEqual(zeitenDocs(kunde.customerId), [], 'die Daten sind trotzdem weg')
|
||||
assert.deepEqual(merkerVon(kunde.features), [])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Loeschfristen (retention.js)
|
||||
|
||||
const abo = (id, customerId, fields) => aw.seed('subscriptions', { $id: id, stripeSubscriptionId: id, customerId, ...fields })
|
||||
|
||||
test('Löschfristen: Daten und Merker der Zeiten eines fälligen Kunden werden gelöscht, dann die Funktion abgeschaltet', async () => {
|
||||
const faellig = await legeKunde({ projekte: 2, news: true })
|
||||
const laufend = await legeKunde()
|
||||
abo(`sub_${faellig.customerId}`, faellig.customerId, { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
|
||||
abo(`sub_${laufend.customerId}`, laufend.customerId, { status: 'active', endedAt: null })
|
||||
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
const eintrag = plan.news.find((n) => n.customerId === faellig.customerId)
|
||||
assert.deepEqual(eintrag.zeitenIds.slice().sort(), faellig.features.map((f) => f.$id).sort())
|
||||
assert.equal(eintrag.featureIds.length, 3, 'zwei Zeiten und News')
|
||||
assert.ok(!plan.news.some((n) => n.customerId === laufend.customerId))
|
||||
assert.match(formatReport(plan), new RegExp(`Kunde ${faellig.customerId}: .*3 Funktion\\(en\\) abzuschalten.*Öffnungszeiten`))
|
||||
|
||||
const result = await applyDeletions(plan)
|
||||
assert.deepEqual(result.failed, [])
|
||||
assert.equal(result.deleted.zeiten, 2)
|
||||
assert.equal(result.deleted.features, 3)
|
||||
assert.match(formatReport(plan, result), /Öffnungszeiten: 2 Datensatz\/Datensätze gelöscht/)
|
||||
assert.deepEqual(zeitenDocs(faellig.customerId), [])
|
||||
assert.deepEqual(merkerVon(faellig.features), [])
|
||||
for (const f of faellig.features) assert.equal(featureDoc(f.$id).state, 'disabled')
|
||||
assert.equal(featureDoc(`f_${faellig.features[0].projectId}_news`).state, 'disabled')
|
||||
|
||||
assert.equal(zeitenDocs(laufend.customerId).length, 1, 'laufendes Abo: nichts angefasst')
|
||||
assert.equal(featureDoc(laufend.features[0].$id).state, 'active')
|
||||
})
|
||||
|
||||
test('Löschfristen: scheitert das Löschen der Zeiten, bleibt die Funktion aktiv (Meldung unter Fehlgeschlagen), ein erneuter Lauf holt es nach', async () => {
|
||||
const kunde = await legeKunde({ projekte: 2 })
|
||||
abo(`sub_${kunde.customerId}`, kunde.customerId, { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
|
||||
aw.failNext({ collection: 'oeffnungszeiten', method: 'DELETE', status: 500, message: 'kaputt' })
|
||||
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
const erster = await applyDeletions(plan)
|
||||
assert.equal(erster.failed.length, 1)
|
||||
assert.equal(erster.failed[0].what, 'oeffnungszeiten')
|
||||
assert.equal(erster.failed[0].id, kunde.customerId)
|
||||
assert.match(erster.failed[0].reason, /kaputt/)
|
||||
assert.equal(erster.deleted.features, 0, 'die Funktionen werden nicht abgeschaltet, solange ihre Daten noch da sind')
|
||||
assert.match(formatReport(plan, erster), /Fehlgeschlagen[\s\S]*oeffnungszeiten .*kaputt/)
|
||||
for (const f of kunde.features) assert.equal(featureDoc(f.$id).state, 'active')
|
||||
assert.equal(zeitenDocs(kunde.customerId).length, 1, 'ein Dokument ist schon weg, eines geblieben')
|
||||
|
||||
const zweiter = await applyDeletions(plan)
|
||||
assert.deepEqual(zweiter.failed, [])
|
||||
assert.equal(zweiter.deleted.zeiten, 1)
|
||||
assert.deepEqual(zeitenDocs(kunde.customerId), [])
|
||||
assert.deepEqual(merkerVon(kunde.features), [])
|
||||
for (const f of kunde.features) assert.equal(featureDoc(f.$id).state, 'disabled')
|
||||
})
|
||||
|
||||
test('Löschfristen: Kunde ohne Zeiten-Funktion bleibt wie bisher (kein zeitenIds, keine Zeile im Bericht)', async () => {
|
||||
nr += 1
|
||||
const customerId = `zl${nr}`
|
||||
abo(`sub_${customerId}`, customerId, { status: 'canceled', endedAt: '2022-03-15T10:00:00.000Z' })
|
||||
aw.seed('portalFeatures', { $id: `f_${customerId}_news`, customerId, projectId: `p_${customerId}`, featureKey: 'news', state: 'active', enabled: true })
|
||||
|
||||
const plan = await findDueDeletions(JAN_2026)
|
||||
const eintrag = plan.news.find((n) => n.customerId === customerId)
|
||||
assert.deepEqual(eintrag.zeitenIds, [])
|
||||
assert.doesNotMatch(formatReport(plan), new RegExp(`Kunde ${customerId}.*Öffnungszeiten`))
|
||||
const result = await applyDeletions(plan)
|
||||
assert.deepEqual(result.failed, [])
|
||||
assert.equal(result.deleted.zeiten, 0)
|
||||
assert.equal(result.deleted.features, 1)
|
||||
assert.doesNotMatch(formatReport(plan, result), /Öffnungszeiten:/)
|
||||
})
|
||||
Reference in new issue
Block a user