Implement audit trail event logging for re-export actions
epic-bufdir-report-history-services-task-008 — Add audit trail event logging to the ReportReexportCoordinator that records every re-export action as an immutable audit event. Each event must capture the acting user ID, organization ID, history entry ID, re-export timestamp, and outcome (success or failure reason). Store events in the designated audit table and ensure logging occurs even when the re-export pipeline itself fails.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 5 - 253 tasks
Can start after Tier 4 completes
Implementation Notes
Wrap the entire re-export pipeline invocation in a `try-catch-finally` block. In the `try` block, run the pipeline. In the `catch` block, capture the exception type and a sanitised message. In the `finally` block, build the audit event and call `auditRepository.insertReexportAuditEvent(event)` — wrap this call in its own `try-catch` that only logs the failure and never rethrows.
A sanitisation helper should strip UUIDs (regex), email addresses (regex), and truncate the message to 500 characters. Extract `userId` and `organizationId` from the injected `AuthSession` — never accept them as method parameters. Define the audit event as an immutable `ReexportAuditEvent` value object. Consider a dedicated `ReexportAuditRepository` rather than reusing the general report history repository, to keep the audit write path isolated.
Do not use `unawaited()` for the audit INSERT — await it to ensure it completes before the coordinator returns.
Testing Requirements
Unit tests (flutter_test) with mock audit repository: (1) on successful re-export — assert audit INSERT called once with outcome='success' and null failure_reason, (2) on pipeline exception — assert audit INSERT called once with outcome='failure' and sanitised failure_reason, and original exception is rethrown after the INSERT, (3) on audit INSERT exception — assert original re-export result or exception is unaffected and application logger receives an error-level message, (4) assert acting_user_id and organization_id come from the injected auth session, not from method parameters. Integration test on staging: trigger a successful re-export and a failed re-export (by passing an invalid history entry ID), then query the audit table and assert both records exist with the correct outcome values.
The ReportReexportCoordinator must invoke the Bufdir export pipeline defined in the bufdir-report-export feature. If that feature's internal API changes (renamed services, altered parameters), the re-export coordinator will break silently at runtime.
Mitigation & Contingency
Mitigation: Define a stable, versioned interface (abstract class or Dart interface) for the export pipeline entry point. The re-export coordinator depends only on this interface, not on concrete export service internals. Document the contract in both features.
Contingency: If the export pipeline breaks the re-export coordinator, fall back to surfacing a clear 'regeneration unavailable' message to the coordinator with instructions to use the primary export screen for the same period as a workaround, while the interface mismatch is fixed.
The audit trail must be immutable — coordinators must not be able to edit or delete past events. If the RLS policies allow UPDATE or DELETE on audit event rows, a coordinator could suppress evidence of a re-export or failed submission.
Mitigation & Contingency
Mitigation: Apply INSERT-only RLS policies to the audit events table (no UPDATE, no DELETE for any non-service-role user). Use a separate service-role key for writing audit events, never the user's JWT. Validate this in integration tests by asserting that UPDATE and DELETE calls from coordinator-role sessions are rejected with RLS errors.
Contingency: If immutability is compromised before detection, run a database audit comparing the audit log against the main history table timestamps to identify tampered records, restore from backup if needed, and issue a patch RLS migration immediately.
The user stories require filter state (year, period type, status) to persist within a session so coordinators do not lose context when navigating away. Implementing this with Riverpod state management could cause stale filter state if the provider is not properly scoped to the session lifecycle.
Mitigation & Contingency
Mitigation: Scope the filter state provider to the router's history route scope, not globally. Use autoDispose with a keepAlive flag tied to the session so filters reset on logout but persist on tab switches within the same session.
Contingency: If filter state becomes stale or leaks between sessions, add an explicit reset in the logout handler that disposes all scoped providers. This is a UX degradation (coordinator must re-apply filters) rather than a data integrity issue.