# Audit-Logging & Best-Effort-Enrichment

> KAR-704 R9 + R12 (Redundanz-Audit 2026-06-08). Stand: 2026-07-03,
> gegen die Prod-DB introspiziert (`pg_trigger`, `information_schema`,
> Code-grep). Zweck: dokumentieren, welche Audit-Daten DB-garantiert
> sind und welche nur so vollständig wie der App-Code, der sie schreibt.

## Audit-/History-Tabellen im Überblick

| Tabelle | Schreibweg | Vollständigkeit |
|---|---|---|
| `audit_log` | **DB-Trigger** `assignments_audit` → `fn_audit_assignments()` auf `assignments` (INSERT/UPDATE/DELETE, old/new als JSONB) | **garantiert** für `assignments` — greift auch bei direkten DB-Writes und Service-Role-Pfaden |
| `audit_log_enriched` | View über `audit_log` + `auth.users` (E-Mail/Name des Verursachers, Status-/Datums-Deltas) | wie `audit_log` |
| `user_audit_log` | App-Code: `app/api/admin/users/**`, `app/api/admin/audit/route.ts` | **app-abhängig** — nur Admin-API-Pfade schreiben; direkte DB-Änderungen erscheinen nicht |
| `intake_status_history` | App-Code: Intake-Detailseite, Demo-Seeder/-Reset | **app-abhängig** |
| `qaf_audit_log` | App-Code: `app/qaf-differences/actions.ts` | **app-abhängig** |
| `master_data_audit_log` | App-Code: `lib/duplicates/merge.ts`, Master-Data-Clients (Repository) | **app-abhängig** |
| `pmo_log_entries` | **kein Writer** (weder App-Code noch Trigger noch RPC), 0 Zeilen in Prod | **tot/legacy** — Drop-Kandidat (eigene Migration + Operator-Entscheidung) |

## Konsequenzen

- Nur `assignments`-Änderungen sind DB-seitig lückenlos auditiert. Alle
  anderen Audit-Tabellen sind Konvention: ein neuer Code-Pfad (oder ein
  Service-Role-Skript), der die Tabelle nicht mitschreibt, hinterlässt
  **stille Lücken** — das ist ein akzeptierter Trade-off, kein Bug.
- Wer einen neuen Write-Pfad für `intakes`, QAF-Vergleiche, Master-Data
  oder User-Verwaltung baut, muss den zugehörigen History-Insert
  mitziehen (Checkliste im PR-Review).
- Soll eine dieser Tabellen DB-garantiert werden, ist das Muster
  vorhanden: Trigger analog `fn_audit_assignments()` (generisch über
  `TG_TABLE_NAME`, old/new JSONB).

## R12: `document_metadata.supplier_id` / `consultant_id` (Ghost-FKs)

`document_metadata` trägt `supplier_id` und `consultant_id` **ohne
FK-Constraint** — bewusst so belassen:

- Die Spalten sind **best-effort enrichment** aus der Dokument-Pipeline;
  ein harter FK würde die Pipeline an unvollständigen/verspäteten
  Stammdaten scheitern lassen.
- Werte können deshalb verwaisen (auf gelöschte Stammdaten zeigen).
  Konsumenten müssen per **LEFT JOIN + null-tolerant** lesen, nie
  INNER JOIN auf diese Spalten.
- Stand 03.07.2026 in Prod: 1 Zeile, beide Spalten NULL — das Risiko
  ist heute theoretisch, die Regel gilt für künftige Pipeline-Läufe.

Entschieden im Redundanz-Audit 2026-06-08 (KAR-704, R12:
„dokumentieren, nicht erzwingen").
