// 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 } // RegisterObjectForSubject ist CMP-02s additive Ergänzung zu // RegisterObject: registriert das Objekt zusätzlich mit einer Referenz // auf die betroffene Person (dataSubjectRef, z. B. E-Mail oder // User-ID), Grundlage für den DSGVO-Auskunftsbericht. Leeres // dataSubjectRef bedeutet: nicht personenbezogen, kein Fehler. // RegisterObject selbst bleibt unverändert (kein Umbau bestehenden // Verhaltens) — dies ist ein separater, additiver Registrierungsweg. func RegisterObjectForSubject(ctx context.Context, pool *pgxpool.Pool, objectType, objectReference, dataSubjectRef string) (string, error) { var id string err := pool.QueryRow(ctx, ` INSERT INTO retention_objects (object_type, object_reference, data_subject_ref) VALUES ($1, $2, NULLIF($3, '')) ON CONFLICT (object_type, object_reference) DO UPDATE SET data_subject_ref = COALESCE(NULLIF(EXCLUDED.data_subject_ref, ''), retention_objects.data_subject_ref) RETURNING id `, objectType, objectReference, dataSubjectRef).Scan(&id) if err != nil { return "", fmt.Errorf("retention: objekt mit betroffener person 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() }