feat(PROJ-51): Aufbewahrungsfristen nach Dokumentenart (Retention-Kategorien)
Fuehrt archiving_rules ein (PROJ-43-Basis: Tabelle + CRUD-API + Admin-UI) und erweitert die Retention-Logik (PROJ-34) um Regel-basierte Fristen, eine globale Mindestfrist (min_retention_days) sowie Nachvollziehbarkeit der Frist-Quelle (retain_until_source) in API und Mail-Detailansicht.
This commit is contained in:
@@ -47,6 +47,7 @@ func runStatus(args []string) {
|
||||
checkStorage(cfg),
|
||||
checkEncryption(cfg),
|
||||
checkAuditLog(cfg),
|
||||
checkRetention(cfg),
|
||||
}
|
||||
|
||||
allOK := true
|
||||
@@ -227,6 +228,41 @@ func checkAuditLog(cfg *config.Config) checkResult {
|
||||
return checkResult{Name: "Audit-Log", OK: true, Detail: detail}
|
||||
}
|
||||
|
||||
// checkRetention warns (PROJ-51) when effectively no deletion lock is active:
|
||||
// global retention_days = 0 AND min_retention_days = 0 AND no tenant with
|
||||
// retention_days > 0 AND no archiving rule carrying a retention_days value.
|
||||
// Like the encryption check it never reports a hard error (OK stays true), it
|
||||
// only surfaces the GoBD-relevant state in the detail text.
|
||||
func checkRetention(cfg *config.Config) checkResult {
|
||||
global := cfg.Storage.RetentionDays
|
||||
minRet := cfg.Storage.MinRetentionDays
|
||||
if global > 0 || minRet > 0 {
|
||||
return checkResult{Name: "Retention", OK: true,
|
||||
Detail: fmt.Sprintf("Löschsperre aktiv (global=%d Tage, min=%d Tage)", global, minRet)}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
pool, err := pgxpool.New(ctx, cfg.Database.DSN())
|
||||
if err != nil {
|
||||
return checkResult{Name: "Retention", OK: true,
|
||||
Detail: "DB nicht erreichbar — Tenant-/Regel-Aufbewahrung nicht prüfbar"}
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
var tenantLocks, ruleLocks int
|
||||
_ = pool.QueryRow(ctx, `SELECT COUNT(*) FROM tenants WHERE retention_days > 0`).Scan(&tenantLocks)
|
||||
_ = pool.QueryRow(ctx, `SELECT COUNT(*) FROM archiving_rules WHERE retention_days IS NOT NULL`).Scan(&ruleLocks)
|
||||
|
||||
if tenantLocks > 0 || ruleLocks > 0 {
|
||||
return checkResult{Name: "Retention", OK: true,
|
||||
Detail: fmt.Sprintf("Löschsperre aktiv (%d Mandant(en), %d Regel(n) mit Aufbewahrung)", tenantLocks, ruleLocks)}
|
||||
}
|
||||
|
||||
return checkResult{Name: "Retention", OK: true,
|
||||
Detail: "WARNUNG — keine Löschsperre aktiv: retention_days=0, min_retention_days=0, keine Mandanten-/Regel-Aufbewahrung (GoBD-relevant)"}
|
||||
}
|
||||
|
||||
func formatBytes(b uint64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
|
||||
@@ -124,8 +124,9 @@ func main() {
|
||||
Dir: cfg.Storage.StorePath,
|
||||
Keyfile: cfg.Storage.Keyfile,
|
||||
DSN: cfg.Database.DSN(),
|
||||
RetentionDays: cfg.Storage.RetentionDays,
|
||||
CompressEnabled: cfg.Storage.Compress,
|
||||
RetentionDays: cfg.Storage.RetentionDays,
|
||||
MinRetentionDays: cfg.Storage.MinRetentionDays,
|
||||
CompressEnabled: cfg.Storage.Compress,
|
||||
}
|
||||
mailStore, err := storage.New(storeCfg)
|
||||
if err != nil {
|
||||
@@ -556,6 +557,12 @@ func runBackfill(ctx context.Context, store *storage.Store, idx index.Indexer, w
|
||||
logger.Warn("backfill: save meta failed", "id", id, "err", err)
|
||||
}
|
||||
|
||||
// PROJ-51: (re-)apply retention for mails without a retain_until yet.
|
||||
// Only sets a value when retain_until IS NULL — never shortens an
|
||||
// existing lock (no retroactive shortening per spec).
|
||||
tenantID, _ := store.GetTenantForMail(ctx, id)
|
||||
store.ApplyRetentionBackfill(ctx, id, pm, tenantID)
|
||||
|
||||
// Check if already indexed
|
||||
alreadyIndexed, err := store.IsIndexed(ctx, id)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user