Buchungskalender: Aufbewahrung und Löschfristen

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:38:35 +00:00
1 parent f57a3d477e
commit c019d3c3b0
9 files changed
+719 -13

No files matched your search

+27 -4
View File
@@ -4,6 +4,7 @@ import { config } from '../config.js'
import { deleteDocument, getDocument, listDocuments, Query, updateDocument } from './appwriteAdmin.js'
import { deleteNewsImage } from './newsImages.js'
import { loescheShopDaten } from './shop/loeschen.js'
import { hatUkDaten, loescheUkDaten } from './unterkunft/aufbewahrung.js'
import { disable } from './siteFeatures.js'
import { getStripe } from './stripeBilling.js'
import { filesOf, STATUS } from './takeoverCredits.js'
@@ -37,6 +38,9 @@ import { filesOf, STATUS } from './takeoverCredits.js'
* 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.
* - Buchungskalender: Fuer faellige Kunden loescht derselbe Lauf auch alle uk*-Daten ihrer Funktionen 'unterkunft' (plan.news[].ukIds, nur wenn es welche gibt;
* Deckel UK_LOESCH_DECKEL je Kunde, Rest = Kunde gehalten). Die eigenen Fristen (ipHash, Anonymisierung, Rechnungen 8 Jahre, 90 Tage nach dem
* Abschalten) laufen im Tagesjob des Buchungskalenders (unterkunft/aufbewahrung.js).
* 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
@@ -51,6 +55,7 @@ const FEATURES = () => config.collections.portalFeatures
const CONSENTS = () => config.collections.featureConsents
const SHOP_CONFIG = () => config.collections.shopConfig
const SHOP_LOESCH_DECKEL = 2000
const UK_LOESCH_DECKEL = 2000
/** Felder am Funktionsdokument, die die Zustimmung zur Bestellung festhalten (leer => nichts zu loeschen). */
const FEATURE_CONSENT_FIELDS = ['consentIp', 'consentUserAgent', 'consentText', 'consentAt']
@@ -154,7 +159,10 @@ export async function findDueDeletions(now = new Date(), { limit = LIST_LIMIT }
const withFields = allFeatures.filter(hasConsentFields)
const consents = await list(CONSENTS(), [Query.equal('customerId', customerId)])
const shops = await shopsOf(customerId, list)
if (!posts.length && !features.length && !consents.length && !withFields.length && !shops.length) continue
// Buchungskalender: Funktionen 'unterkunft' mit uk*-Daten (auch abgeschaltete); ohne Collections (Setup nicht gelaufen) gibt es keine
const ukIds = []
for (const f of allFeatures.filter((x) => x.featureKey === 'unterkunft')) if (await hatUkDaten(f.$id)) ukIds.push(f.$id)
if (!posts.length && !features.length && !consents.length && !withFields.length && !shops.length && !ukIds.length) continue
news.push({
customerId,
posts: posts.map((p) => ({ id: p.$id, projectId: p.projectId, imageId: p.imageId || null })),
@@ -162,6 +170,7 @@ export async function findDueDeletions(now = new Date(), { limit = LIST_LIMIT }
consentIds: consents.map((c) => c.$id),
featureFieldIds: withFields.map((f) => f.$id),
...(shops.length ? { shops } : {}),
...(ukIds.length ? { ukIds } : {}),
zeitenIds: features.filter((f) => f.featureKey === 'zeiten').map((f) => f.$id),
})
}
@@ -216,12 +225,12 @@ async function deleteQuietly(collection, id) {
* 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, featureConsents: number, featureFields: number, shops: number, shopDocs: number,
* zeiten: number },
* zeiten: number, unterkuenfte: number, ukDocs: number },
* manual: { customerId: string, subscriptionId: string, reason: string }[],
* failed: { what: string, id: string, reason: string }[], held: string[] }>}
*/
export async function applyDeletions(plan) {
const deleted = { subscriptions: 0, contractEvents: 0, takeover: 0, stripeCleared: 0, newsPosts: 0, newsImages: 0, features: 0, featureConsents: 0, featureFields: 0, shops: 0, shopDocs: 0, zeiten: 0 }
const deleted = { subscriptions: 0, contractEvents: 0, takeover: 0, stripeCleared: 0, newsPosts: 0, newsImages: 0, features: 0, featureConsents: 0, featureFields: 0, shops: 0, shopDocs: 0, zeiten: 0, unterkuenfte: 0, ukDocs: 0 }
const manual = []
const failed = []
const held = []
@@ -286,6 +295,18 @@ export async function applyDeletions(plan) {
for (const id of entry.consentIds || []) {
if (await attempt('featureConsents', id, () => deleteQuietly(CONSENTS(), id))) deleted.featureConsents += 1
}
// Buchungskalender: alle uk*-Daten der Funktion (Anfragen, Rechnungen, Sperren, Preise, Unterkuenfte, Einstellungen); gemeinsamer Deckel je Kunde
let ukRest = UK_LOESCH_DECKEL
for (const featureId of entry.ukIds || []) {
await attempt('unterkunft', featureId, async () => {
const weg = await loescheUkDaten({ featureId, deckel: ukRest })
ukRest -= weg.geloescht
deleted.ukDocs += weg.geloescht
if (!weg.vollstaendig) throw new Error('Buchungsdaten nicht vollständig gelöscht (Deckel je Lauf), der nächste Lauf macht weiter')
deleted.unterkuenfte += 1
return true
})
}
// Webshop-Daten: Bestellungen, Artikel, Widerrufe, Rechtstexte-Archiv, Bilder, zuletzt shopConfig (Deckel je Kunde; Rest = der Kunde wird gehalten)
let shopRest = SHOP_LOESCH_DECKEL
for (const shop of entry.shops || []) {
@@ -366,9 +387,10 @@ export function formatReport(plan, result = null) {
const consents = n.consentIds?.length || 0
const fields = n.featureFieldIds?.length || 0
const shops = n.shops?.length || 0
const uk = n.ukIds?.length ? `; ${n.ukIds.length} Buchungskalender samt Anfragen, Rechnungen und Unterkünften zu löschen` : ''
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, ${consents} Zustimmungsnachweis(e) zu löschen, ${fields} Funktionsdokument(e) mit Zustimmungsangaben zu leeren${shops ? `, ${shops} Webshop(s) samt Bestellungen, Artikeln, Widerrufen und Bildern zu löschen` : ''}${zeiten}`
`Kunde ${n.customerId}: ${n.posts.length} News-Beitrag/-Beiträge mit ${images} Foto(s) zu löschen, ${n.featureIds.length} Funktion(en) abzuschalten, ${consents} Zustimmungsnachweis(e) zu löschen, ${fields} Funktionsdokument(e) mit Zustimmungsangaben zu leeren${shops ? `, ${shops} Webshop(s) samt Bestellungen, Artikeln, Widerrufen und Bildern zu löschen` : ''}${uk}${zeiten}`
)
}
const del = plan.takeover.filter((t) => t.action === 'loeschen')
@@ -391,6 +413,7 @@ export function formatReport(plan, result = null) {
`Zustimmungsangaben an Funktionsdokumenten (IP, Browser, Text, Zeit): ${d.featureFields} geleert.`,
...(d.shops || d.shopDocs ? [`Webshop: ${d.shops} Shop(s) gelöscht (${d.shopDocs} Dokument(e) in Bestellungen, Artikeln und Widerrufen).`] : [])
)
if (d.unterkuenfte || d.ukDocs) lines.push(`Buchungskalender: ${d.unterkuenfte} Funktion(en) gelöscht (${d.ukDocs} Dokument(e)).`)
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}`))