internal/retention: object_type/object_reference als reine Textfelder (Adapter-Muster, keine Fremdschluessel auf DMS-/Mail-Tabellen). Aufbewahrungsklassen-Zuordnung historisiert (jede Zuordnung eigene, unveraenderliche Zeile). Migration real vorwaerts+rueckwaerts gegen die tatsaechlichen .sql-Dateien getestet, Mandantentrennung gegen echtes zweites Tenant-DB bewiesen. Grundlage fuer RET-05 (Adapter- Interface) und RET-02.
104 lines
3.6 KiB
Go
104 lines
3.6 KiB
Go
// Package retention implementiert RET-01: ein generisches Datenmodell
|
|
// für aufbewahrungspflichtige Objekte, modulübergreifend über Adapter
|
|
// (Objekttyp + Objekt-Referenz als reine Textfelder) — Archive kennt die
|
|
// Fachobjekte anderer Module (DMS, Mail) nicht im Detail, nur ihren Typ
|
|
// und ihre Referenz. Keine Fremdschlüssel auf modulspezifische Tabellen,
|
|
// damit ein neues Modul retention-pflichtige Objekte einbinden kann,
|
|
// ohne dieses Paket zu ändern.
|
|
package retention
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Status eines Retention-Objekts.
|
|
type Status string
|
|
|
|
const (
|
|
StatusActive Status = "active"
|
|
StatusExpired Status = "expired"
|
|
StatusDeleted Status = "deleted"
|
|
)
|
|
|
|
// RegisterObject registriert ein Objekt eines beliebigen Moduls unter
|
|
// seinem Typ+Referenz — idempotent (ON CONFLICT), ein Adapter kann ein
|
|
// bereits bekanntes Objekt gefahrlos erneut registrieren
|
|
// (Akzeptanzkriterium 1: bildet beliebige Objekttypen ab, ohne
|
|
// modulspezifische Spalten).
|
|
func RegisterObject(ctx context.Context, pool *pgxpool.Pool, objectType, objectReference string) (string, error) {
|
|
var id string
|
|
err := pool.QueryRow(ctx, `
|
|
INSERT INTO retention_objects (object_type, object_reference)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT (object_type, object_reference) DO UPDATE SET object_type = EXCLUDED.object_type
|
|
RETURNING id
|
|
`, objectType, objectReference).Scan(&id)
|
|
if err != nil {
|
|
return "", fmt.Errorf("retention: objekt registrieren: %w", err)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// Assignment ist EINE historische Zuordnung einer Aufbewahrungsklasse.
|
|
type Assignment struct {
|
|
RetentionClass string
|
|
AssignedAt time.Time
|
|
}
|
|
|
|
// AssignClass ordnet einem Retention-Objekt eine neue Aufbewahrungsklasse
|
|
// zu — fügt IMMER eine neue Zeile hinzu, ändert nie eine bestehende
|
|
// (Akzeptanzkriterium 2: historisierbar).
|
|
func AssignClass(ctx context.Context, pool *pgxpool.Pool, retentionObjectID, retentionClass string) error {
|
|
_, err := pool.Exec(ctx, `
|
|
INSERT INTO retention_class_assignments (retention_object_id, retention_class)
|
|
VALUES ($1, $2)
|
|
`, retentionObjectID, retentionClass)
|
|
if err != nil {
|
|
return fmt.Errorf("retention: aufbewahrungsklasse zuordnen: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// CurrentClass liefert die AKTUELLE Aufbewahrungsklasse (jüngste
|
|
// Zuordnung) eines Retention-Objekts.
|
|
func CurrentClass(ctx context.Context, pool *pgxpool.Pool, retentionObjectID string) (Assignment, error) {
|
|
var a Assignment
|
|
err := pool.QueryRow(ctx, `
|
|
SELECT retention_class, assigned_at FROM retention_class_assignments
|
|
WHERE retention_object_id = $1
|
|
ORDER BY assigned_at DESC LIMIT 1
|
|
`, retentionObjectID).Scan(&a.RetentionClass, &a.AssignedAt)
|
|
if err != nil {
|
|
return Assignment{}, fmt.Errorf("retention: aktuelle aufbewahrungsklasse lesen: %w", err)
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
// ClassHistory liefert ALLE Zuordnungen eines Retention-Objekts,
|
|
// chronologisch aufsteigend — voller Nachvollzug der Historie.
|
|
func ClassHistory(ctx context.Context, pool *pgxpool.Pool, retentionObjectID string) ([]Assignment, error) {
|
|
rows, err := pool.Query(ctx, `
|
|
SELECT retention_class, assigned_at FROM retention_class_assignments
|
|
WHERE retention_object_id = $1
|
|
ORDER BY assigned_at ASC
|
|
`, retentionObjectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("retention: klassenhistorie lesen: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var history []Assignment
|
|
for rows.Next() {
|
|
var a Assignment
|
|
if err := rows.Scan(&a.RetentionClass, &a.AssignedAt); err != nil {
|
|
return nil, fmt.Errorf("retention: historien-zeile lesen: %w", err)
|
|
}
|
|
history = append(history, a)
|
|
}
|
|
return history, rows.Err()
|
|
}
|