# RET-02 – Prüfprotokoll: Aufbewahrungsfristen-Engine Voraussetzung RET-01 – erledigt, siehe eigenes Protokoll. **Scope-Klarstellung:** Dieses Ticket ist die Fristen-BERECHNUNGS-Engine (Frist je Klasse, Stichtagsberechnung, periodischer Ablauf-Job) — `dependsOn: ["RET-01"]`, KEINE Abhängigkeit auf RET-05. Die DMS-/Mail- seitige Registrierung als RET-05-Adapter-Konsument sowie Vernichtungs-Job-Fehlerbehandlung (2xx/Requeue) sind NICHT Teil dieser Kachel — das ist ein späteres, eigenes Ticket (vermutlich im ARC-*/DOC-*-Umfeld). Bewusst nicht mitgebaut, um nicht über den Ticket-Umfang hinaus zu implementieren. ## Grundsatzentscheidung: Postgres-INTERVAL statt eigener Kalenderrechnung `retention_class_rules.duration` ist ein natives Postgres-`INTERVAL` (z. B. `'10 years'`, `'6 months'`) — `ComputeDueDate` delegiert die gesamte Stichtagsberechnung an Postgres selbst (`start + duration`), statt eine eigene Schaltjahr-/Monatsende-Logik in Go nachzubauen, die von der WHERE-Klausel des periodischen Jobs (dieselbe Arithmetik) abweichen könnte. Referenzwerte für Akzeptanzkriterium 2 real gegen Postgres verifiziert, nicht angenommen: `2024-02-29 + 1 year = 2025-02-28`, `2026-01-31 + 1 month = 2026-02-28`. ## Umsetzung - `migrations/0004_retention_class_rules.up.sql`/`.down.sql`. - `internal/retentionengine.ConfigureClassRule` — eine Regel je Klasse (`UPSERT`). - `internal/retentionengine.ComputeDueDate` — delegiert an Postgres. - `internal/retentionengine.ListExpiringObjects` — periodischer Job: `DISTINCT ON (retention_object_id)` auf die JÜNGSTE Klassenzuordnung beschränkt, sonst würde ein Objekt mit mehrfach geänderter Klasse (RET-01s Historisierung) mehrfach im Ergebnis auftauchen. ## Prüfungen | # | Prüfung | Ergebnis | |---|---|---| | 1 | Fristberechnung an Referenzdaten mit bekannten Ablaufdaten geprüft | **bestanden** — `TestComputeDueDate_KnownReferenceDates`: Schaltjahr (29.02.2024 + 1 Jahr → 28.02.2025) und Monatsende (31.01.2026 + 1 Monat → 28.02.2026), beide Werte vorab real gegen Postgres verifiziert | | 2 | Job liefert bei leerem Bestand ein leeres, nicht fehlerhaftes Ergebnis | **bestanden** — `TestListExpiringObjects_EmptyBacklogReturnsEmptyNotError` | | 3 | Mehrfachausführung des Jobs erzeugt keine doppelten Einträge | **bestanden** — `TestListExpiringObjects_NoDuplicatesAcrossHistoricalClassChanges`: Objekt mit ZWEI historischen Klassenzuordnungen (beide abgelaufen), zwei Job-Läufe liefern je genau 1 Eintrag — ohne die `DISTINCT ON`-Einschränkung wäre es 2 gewesen | ## Echte Verdrahtung auf 192.168.1.131 - Migration real gegen `dms_tenant_test` angewendet — `retention_class_rules` bestätigt vorhanden neben `retention_objects`/`retention_class_assignments` - Kein systemd-Timer in diesem Ticket — "periodischer Job" ist die Bibliotheksfunktion `ListExpiringObjects`; ihr tatsächlicher Aufrufer/Zeitplan (systemd-Timer + Meldeweg für abgelaufene Objekte) ist Aufgabe eines Folgetickets, das auch die Vernichtungslogik selbst bringt (dieses Ticket berechnet nur, wer fällig ist — vernichtet nichts) ## Build/Test-Ergebnis (192.168.1.131, `make check`) ``` go build ./... -> clean go vet ./... -> clean golangci-lint run ./... -> 0 issues go test ./... -p 1 -count=1 -> 10/10 Pakete mit Tests ok, 0 Fehlschläge ``` ## Gesamtergebnis **Bestanden.** Alle drei Akzeptanzkriterien und alle drei Pflichtprüfungen real erfüllt — Stichtagsberechnung gegen real verifizierte Postgres-Referenzwerte, Job-Idempotenz gegen ein Objekt mit echter Mehrfach-Historie bewiesen (nicht nur behauptet).