Terminplaner: Aufbewahrungs-Loeschung seitenweise (Grundlagen, verfallen/abgesagt/abgelehnt)

loeschKandidaten liest eine Seite (Standard 100, aelteste Bezugszeit zuerst, optional je Funktion)
ohne Gastfelder; die Frist steht in der Abfrage. arbeiteSeiten (Seite lesen, bearbeiten, neu lesen)
endet bei leerer Seite, am Deckel (config.termine.loeschDeckel, 2000 je Status und Lauf, Rest an den
Folgetagen, gezaehlt als gedeckelt) oder bei einer Seite ohne Fortschritt. verfallen, abgesagt und
abgelehnt laufen darueber, unter der Buchungssperre werden Status und Bezugszeit frisch geprueft.
taeglicherJob liefert gedeckelt getrennt vom Loeschzaehler. Neue Indizes idx_status_ends und
idx_status_updated auf termineBookings (scripts/termine-setup.mjs legt sie an).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 08:55:32 +00:00
1 parent 2dd9c0dec7
commit 5c95dfc322
8 files changed
+484 -33

No files matched your search

+53
View File
@@ -958,6 +958,59 @@ test('loeschKandidaten: Bezugszeit je Status (verfallen heldUntil, erledigt Term
await assert.rejects(B.loeschKandidaten('gehalten', neunzig), TypeError)
})
test('loeschKandidaten: hoechstens limit (Standard 100), aelteste Bezugszeit zuerst, nach Funktion filterbar, ohne Gastfelder', async () => {
const mit = (ms) => iso(new Date(JETZT.getTime() + ms))
const neunzig = new Date(JETZT.getTime() - 90 * TAG)
const nr = (i) => String(i).padStart(3, '0')
// 150 alte erledigt; je groesser i, desto spaeter das Terminende; in umgekehrter Reihenfolge angelegt, jede dritte in der Funktion 2
for (let i = 149; i >= 0; i -= 1) {
seedBuchung(`b_e${nr(i)}`, { status: 'erledigt', endsAt: mit(-91 * TAG + i * MIN), featureId: i % 3 === 0 ? 'f_feature02' : F })
}
seedBuchung('b_jung', { status: 'erledigt', endsAt: mit(-89 * TAG) })
const ids = (liste) => liste.map((doc) => doc.$id)
const alle = Array.from({ length: 150 }, (_, i) => `b_e${nr(i)}`)
aw.resetListCalls()
const standard = await B.loeschKandidaten('erledigt', neunzig)
assert.deepEqual(ids(standard), alle.slice(0, 100), 'Standard 100, aelteste zuerst')
assert.deepEqual(ids(await B.loeschKandidaten('erledigt', neunzig, { limit: 10 })), alle.slice(0, 10))
assert.deepEqual(ids(await B.loeschKandidaten('erledigt', neunzig, { limit: 1 })), [alle[0]])
const zwei = await B.loeschKandidaten('erledigt', neunzig, { featureId: 'f_feature02' })
assert.deepEqual(ids(zwei), alle.filter((_, i) => i % 3 === 0), 'nur die Funktion 2, aelteste zuerst')
assert.deepEqual(ids(await B.loeschKandidaten('erledigt', neunzig, { featureId: 'f_feature02', limit: 5 })), alle.filter((_, i) => i % 3 === 0).slice(0, 5))
assert.deepEqual(await B.loeschKandidaten('erledigt', neunzig, { featureId: 'f_gibt_es_nicht' }), [])
// keine Gastfelder: weder Name noch Adresse, nur Kennung, Funktion, Status und die drei Bezugszeiten
const erlaubt = new Set(['featureId', 'status', 'endsAt', 'updatedAt', 'heldUntil'])
for (const doc of standard) {
assert.ok(doc.$id)
assert.equal(doc.status, 'erledigt')
assert.deepEqual(Object.keys(doc).filter((key) => !key.startsWith('$') && !erlaubt.has(key)), [], `${doc.$id}: nur erlaubte Felder`)
assert.equal('gastName' in doc, false)
assert.equal('gastEmail' in doc, false)
}
// die Abfrage sagt es selbst: select ohne Gastfelder, limit, Reihenfolge nach der Bezugszeit
const abfrage = aw.listCalls('termineBookings')[0].queries
assert.deepEqual(abfrage.find((q) => q.method === 'select').values, ['$id', 'featureId', 'status', 'endsAt', 'updatedAt', 'heldUntil'])
assert.deepEqual(abfrage.find((q) => q.method === 'limit').values, [100])
assert.deepEqual(abfrage.find((q) => q.method === 'orderAsc'), { method: 'orderAsc', attribute: 'endsAt' })
assert.deepEqual(abfrage.find((q) => q.method === 'lessThan'), { method: 'lessThan', attribute: 'endsAt', values: [iso(neunzig)] })
// Reihenfolge nach der Bezugszeit des Status: verfallen heldUntil, abgesagt updatedAt (Einfuegereihenfolge ist egal)
seedBuchung('b_v2', { status: 'verfallen', heldUntil: mit(-60 * STUNDE) })
seedBuchung('b_v1', { status: 'verfallen', heldUntil: mit(-70 * STUNDE) })
seedBuchung('b_v3', { status: 'verfallen', heldUntil: mit(-50 * STUNDE) })
assert.deepEqual(ids(await B.loeschKandidaten('verfallen', new Date(JETZT.getTime() - 48 * STUNDE))), ['b_v1', 'b_v2', 'b_v3'])
seedBuchung('b_a2', { status: 'abgesagt', updatedAt: mit(-40 * TAG) })
seedBuchung('b_a1', { status: 'abgesagt', updatedAt: mit(-50 * TAG) })
assert.deepEqual(ids(await B.loeschKandidaten('abgesagt', new Date(JETZT.getTime() - 30 * TAG))), ['b_a1', 'b_a2'])
// wie bisher: nicht loeschbare Status und ungueltige limit
await assert.rejects(B.loeschKandidaten('bestaetigt', neunzig, { limit: 10 }), TypeError)
await assert.rejects(B.loeschKandidaten('gehalten', neunzig), TypeError)
for (const limit of [0, -1, 1.5, NaN, '10']) await assert.rejects(B.loeschKandidaten('erledigt', neunzig, { limit }), TypeError, `limit ${limit}`)
})
test('zaehleAnfragen: offene Anfragen einer Funktion (auch ueber 25)', async () => {
for (let i = 0; i < 27; i += 1) seedBuchung(`b_anf${String(i).padStart(2, '0')}`, { status: 'angefragt' })
seedBuchung('b_best', { status: 'bestaetigt' })