Buchungskalender: Aufbewahrung – Sperre wie Ablauf, Restpunkte

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:43:44 +00:00
1 parent c019d3c3b0
commit 827ee54875
5 files changed
+102 -17

No files matched your search

-2
View File
@@ -1098,8 +1098,6 @@ export const config = {
unverbindlichMonate: 6,
// Offene (neue) Anfragen verfallen nach so vielen Tagen
neuVerfallTage: 60,
// Tage nach Abschalten der Funktion bis zur Loeschung aller Buchungsdaten (Tagesjob)
loeschTageNachAbschalten: 90,
// Hoechstzahl Schreib-/Loeschvorgaenge der Aufbewahrung je Tageslauf
aufbewahrungDeckel: 500,
// Anfragen werden so viele Jahre nach Abschluss anonymisiert; Rechnungen bleiben rechnungJahre Jahre
+23 -8
View File
@@ -26,7 +26,7 @@ const SEITE = 100
const ANONYM_JAHRE = 3
const RECHNUNG_JAHRE = 8
const MARKER = 'Buchungsdaten gelöscht am '
const MARKER_RE = /^Buchungsdaten gelöscht am (\d{4}-\d{2}-\d{2})/
const MARKER_RE = /Buchungsdaten gelöscht am (\d{4}-\d{2}-\d{2})/
const ANFRAGE_ANONYM = { gast: {}, nachrichtVermieter: '', ipHash: '' }
const zeitMs = (v) => {
@@ -35,7 +35,8 @@ const zeitMs = (v) => {
}
const jahrVon = (tag) => Number(String(tag).slice(0, 4))
const gueltigerTag = (tag) => typeof tag === 'string' && /^\d{4}-\d{2}-\d{2}/.test(tag)
const fehlerText = (err) => `${err?.name}${err?.status ? ` ${err.status}` : ''}: ${String(err?.message ?? '').slice(0, 160)}`
/** Nur Name, Status und Code: die Meldung kann Personendaten enthalten und kommt nie ins Log. */
const fehlerText = (err) => [err?.name, err?.status, err?.code ?? err?.type].filter((x) => x !== undefined && x !== null && x !== '').join(' ')
const sammlungFehlt = (err) => err?.type === 'collection_not_found' || err?.code === 'collection_not_found'
/** now minus n Kalendermonate (UTC, Tag wird auf den Monatsletzten gekuerzt). */
@@ -86,13 +87,14 @@ async function anfragenMitRechnung() {
* Tagesarbeit der Aufbewahrung.
* @param {Date} now
* @param {{ deckel?: number }} [opt] hoechstens so viele Schreib-/Loeschvorgaenge (Standard 500)
* @returns {Promise<{ anfragen: number, gaeste: number, ipHashes: number, abgelaufen: number, rechnungen: number, adressen: number, fehler: number, gedeckelt: boolean }>}
* @returns {Promise<{ verbraucht: number (genutzter Deckel), anfragen: number, gaeste: number, ipHashes: number, abgelaufen: number, rechnungen: number, adressen: number, fehler: number, gedeckelt: boolean }>}
* anfragen = unverbindliche Anfragen anonymisiert, gaeste = Gastdaten gebuchter Aufenthalte anonymisiert
*/
export async function anonymisiere(now, { deckel = config.unterkunft.aufbewahrungDeckel ?? STANDARD_DECKEL } = {}) {
const nowMs = nowMsVon(now)
const erg = { anfragen: 0, gaeste: 0, ipHashes: 0, abgelaufen: 0, rechnungen: 0, adressen: 0, fehler: 0, gedeckelt: false }
const erg = { anfragen: 0, gaeste: 0, ipHashes: 0, abgelaufen: 0, rechnungen: 0, adressen: 0, fehler: 0, gedeckelt: false, verbraucht: 0 }
let rest = Number.isInteger(deckel) && deckel > 0 ? deckel : STANDARD_DECKEL
const start = rest
const jetztIso = new Date(nowMs).toISOString()
const AN = config.collections.ukAnfragen
const c = config.unterkunft
@@ -135,9 +137,9 @@ export async function anonymisiere(now, { deckel = config.unterkunft.aufbewahrun
// 2. neu, nach neuVerfallTage nicht entschieden: abgelaufen (ohne Mail)
await schritt('neu verfallen', async () => {
const grenze = nowMs - c.neuVerfallTage * TAG_MS
const gefunden = await sammle(AN, [Query.equal('status', 'neu'), Query.lessThan('$createdAt', new Date(grenze).toISOString())], ['status', '$createdAt'], (d) => zeitMs(d.$createdAt) < grenze, rest)
const gefunden = await sammle(AN, [Query.equal('status', 'neu'), Query.lessThan('$createdAt', new Date(grenze).toISOString())], ['status', 'unterkunftId', '$createdAt'], (d) => zeitMs(d.$createdAt) < grenze, rest)
await bearbeite('neu verfallen', gefunden, async (d) => {
const neu = await withLocks(['uk-aufbewahrung:' + d.$id], async () => {
const neu = await withLocks(['unterkunft:' + d.unterkunftId], async () => { // derselbe Schluessel wie ablauf.js (gibFrei/lehneAb/Fristen)
const a = await ladeAnfrage(d.$id)
if (!a || a.status !== 'neu') return null
return aktualisiereAnfrage(a.$id, { status: 'abgelaufen', abgelaufenAt: jetztIso, ipHash: '' })
@@ -177,7 +179,8 @@ export async function anonymisiere(now, { deckel = config.unterkunft.aufbewahrun
rest,
)
await bearbeite('anonymisieren', gebucht, async (d) => {
await aktualisiereAnfrage(d.$id, { ...ANFRAGE_ANONYM, anonymisiertAt: jetztIso })
// ohne Rechnung gibt es keinen Beleg, der die Rechnungsadresse schuetzt: sie geht mit den Gastdaten
await aktualisiereAnfrage(d.$id, { ...ANFRAGE_ANONYM, ...(hatRechnung(d) ? {} : { rechnungsadresse: null }), anonymisiertAt: jetztIso })
erg.gaeste += 1
return true
})
@@ -224,6 +227,7 @@ export async function anonymisiere(now, { deckel = config.unterkunft.aufbewahrun
}
})
erg.verbraucht = start - rest
return erg
}
@@ -285,6 +289,14 @@ export async function hatUkDaten(featureId) {
return false
}
/** Marker an die bisherige Notiz haengen (eine alte Marke wird ersetzt, der Rest bleibt; hoechstens 300 Zeichen, der Marker bleibt ganz). */
function markiere(notiz, tag) {
const marker = `${MARKER}${tag}`
const alt = String(notiz ?? '').replace(/\s*(?:[-–|]\s*)?Buchungsdaten gelöscht am \d{4}-\d{2}-\d{2}/g, '').trim()
if (!alt) return marker
return `${alt.slice(0, Math.max(0, 300 - marker.length - 3))} – ${marker}`
}
/**
* Abgeschaltete Buchungskalender (portalFeatures.state disabled, disabledAt aelter als loeschTageNachAbschalten = 90 Tage): alle uk*-Daten
* loeschen, danach Marker in adminNote (wie beim Shop; ein spaeteres Abschalten setzt disabledAt neu und gilt wieder).
@@ -318,7 +330,10 @@ export async function loescheAbgeschaltete(now, { deckel = STANDARD_DECKEL } = {
if (f.siteKey) leereCache(f.siteKey)
if (weg.vollstaendig) {
erg.funktionen += 1
if (await nochAus()) await updateDocument(FEATURES, f.$id, { adminNote: `${MARKER}${heute(new Date(nowMs))}`.slice(0, 300) })
const frisch = await getDocument(FEATURES, f.$id)
if (frisch.state === 'disabled' && zeitMs(frisch.disabledAt) < grenze) {
await updateDocument(FEATURES, f.$id, { adminNote: markiere(frisch.adminNote, heute(new Date(nowMs))) })
}
} else if (!weg.abgebrochen) {
erg.gedeckelt = true
}
+7 -5
View File
@@ -22,7 +22,7 @@ async function einzeln(name, arbeit) {
try {
return await arbeit()
} catch (err) {
console.error(`[unterkunft-jobs] ${name} fehlgeschlagen: ${err?.name}: ${String(err?.message).slice(0, 200)}`)
console.error(`[unterkunft-jobs] ${name} fehlgeschlagen: ${err?.name} ${err?.status ?? ''}`)
return { fehler: true }
} finally {
laufend.delete(name)
@@ -55,7 +55,7 @@ async function gibtDaten() {
/**
* Tagesjob: Aufbewahrung (ipHash, Verfall von `neu`, Anonymisierung, Rechnungen nach 8 Jahren) und Loeschung abgeschalteter Funktionen
* (90 Tage). Gemeinsamer Deckel config.unterkunft.aufbewahrungDeckel (500) je Lauf. Bei UNTERKUNFT_ENABLED=off nur, wenn es noch
* (90 Tage). Gemeinsamer Deckel config.unterkunft.aufbewahrungDeckel (500) je Lauf (die Loeschung bekommt, was die Aufbewahrung uebrig laesst). Bei UNTERKUNFT_ENABLED=off nur, wenn es noch
* Buchungsdaten gibt (die Fristen gelten auch dann). Ein Teil, der scheitert, stoppt den anderen nicht.
*/
export async function taeglicherJob(now = new Date()) {
@@ -64,15 +64,17 @@ export async function taeglicherJob(now = new Date()) {
const deckel = config.unterkunft.aufbewahrungDeckel ?? 500
let anon = { anfragen: 0, gaeste: 0, fehler: 1 }
let weg = { funktionen: 0, dokumente: 0, fehler: 1, gedeckelt: false }
let rest = deckel
try {
anon = await anonymisiere(now, { deckel })
rest = Math.max(0, deckel - (anon.verbraucht ?? 0))
} catch (err) {
console.error(`[unterkunft-jobs] taeglicherJob: Aufbewahrung fehlgeschlagen: ${err?.name}: ${String(err?.message).slice(0, 200)}`)
console.error(`[unterkunft-jobs] taeglicherJob: Aufbewahrung fehlgeschlagen: ${err?.name} ${err?.status ?? ''}`)
}
try {
weg = await loescheAbgeschaltete(now, { deckel })
weg = await loescheAbgeschaltete(now, { deckel: rest })
} catch (err) {
console.error(`[unterkunft-jobs] taeglicherJob: Löschung abgeschalteter Funktionen fehlgeschlagen: ${err?.name}: ${String(err?.message).slice(0, 200)}`)
console.error(`[unterkunft-jobs] taeglicherJob: Löschung abgeschalteter Funktionen fehlgeschlagen: ${err?.name} ${err?.status ?? ''}`)
}
return { aufbewahrung: anon, abgeschaltet: weg }
})