critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ExportAuditRepository is a Dart class with a constructor accepting SupabaseClient as a dependency
insertAuditEntry(ExportAuditEntry entry) inserts a row into bufdir_export_audit and returns the inserted entry with server-generated id and created_at
insertAuditEntry throws AuditInsertException if the Supabase call returns a PostgrestException
getAuditLogForOrg(String orgId, DateTimeRange period) returns a List<ExportAuditEntry> ordered by created_at descending, filtered by org_id and created_at within the given DateTimeRange
getAuditLogForOrg throws ArgumentError if orgId is empty or DateTimeRange.start is after DateTimeRange.end
getEntryById(String exportId) returns a single ExportAuditEntry or null if not found
getEntryById throws ArgumentError if exportId is not a valid UUID format
No UPDATE or DELETE methods exist on the class — the class surface enforces append-only semantics at the Dart layer
All methods include org_id as an explicit filter in the Supabase query (.eq('org_id', orgId)) — never rely solely on RLS for scoping
ExportAuditEntry is a freezed/data class with fields: id (String), org_id (String), export_id (String), performed_by (String), action (ExportAuditAction enum), file_path (String?), created_at (DateTime)
The repository is registered as a Riverpod provider (Provider<ExportAuditRepository>) for use across the app

Technical Requirements

frameworks
Dart (latest)
Riverpod (provider registration)
supabase_flutter (SupabaseClient)
freezed (immutable data class for ExportAuditEntry)
apis
Supabase PostgREST: from('bufdir_export_audit').insert()
Supabase PostgREST: from('bufdir_export_audit').select().eq().gte().lte().order()
Supabase PostgREST: from('bufdir_export_audit').select().eq('id', exportId).maybeSingle()
data models
ExportAuditEntry (id, org_id, export_id, performed_by, action, file_path, created_at)
ExportAuditAction enum (initiated, completed, failed, downloaded)
bufdir_export_audit table
performance requirements
getAuditLogForOrg must apply server-side pagination — accept optional limit (default 50) and offset parameters
Query must use indexed columns (org_id, created_at) — confirm index exists before merging
security requirements
org_id filter must always be applied explicitly in queries — defence in depth alongside RLS
exportId must be validated as UUID before use in queries to prevent injection via malformed strings
SupabaseClient must be the authenticated instance (not anonymous) — assert client.auth.currentUser != null at construction time or in each method

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place the class at lib/features/bufdir_reporting/data/repositories/export_audit_repository.dart following the existing feature-layer folder convention. Define a custom AuditInsertException extending AppException for consistent error handling. The ExportAuditEntry data class should use json_serializable or freezed with fromJson/toJson so it can be deserialized directly from Supabase's Map response. For the Riverpod provider, use a simple Provider that reads supabaseClientProvider — do not use StateNotifier here as the repository is stateless.

When calling .insert(), use .select() chained after insert to retrieve the server-generated id and created_at in a single round trip (Supabase supports this). Avoid DateTime.now() for created_at — always use the server-returned value.

Testing Requirements

Unit tests using flutter_test with a mocked SupabaseClient (use mockito or mocktail). Test: (1) insertAuditEntry calls insert with correct payload including org_id; (2) insertAuditEntry propagates PostgrestException as AuditInsertException; (3) getAuditLogForOrg applies eq('org_id'), gte('created_at'), lte('created_at'), and order('created_at', ascending: false); (4) getAuditLogForOrg throws ArgumentError for empty orgId; (5) getAuditLogForOrg throws ArgumentError when period.start > period.end; (6) getEntryById returns null when maybeSingle() returns null; (7) getEntryById throws ArgumentError for non-UUID string. Integration tests (separate file) against real Supabase test instance: verify actual insert, read-back, and that a direct UPDATE attempt via the Dart client throws due to RLS. Minimum 80% line coverage on the repository class.

Component
Export Audit Log Repository
data low
Epic Risks (2)
high impact medium prob security

RLS policies for the audit log and schema config tables must correctly handle multi-chapter membership hierarchies (up to 1,400 local chapters for NHF). Incorrect policies could either over-expose data across organisations or prevent legitimate coordinator access, both of which are serious compliance failures.

Mitigation & Contingency

Mitigation: Design RLS policies using the existing org hierarchy resolver pattern. Write integration tests that verify cross-organisation isolation with representative fixture data covering NHF's multi-level hierarchy before merging.

Contingency: If RLS policies prove too complex to express safely in Postgres, implement a Supabase Edge Function as a data access proxy that enforces isolation in application code, with RLS serving as a secondary defence layer.

medium impact medium prob scope

Bufdir's column schema is expected to evolve as Norse Digital Products negotiates a simplified digital reporting format. If the schema config versioning model is too rigid, applying Bufdir schema updates without a code deployment could fail, forcing emergency releases.

Mitigation & Contingency

Mitigation: Design the schema config table to store the full JSON column mapping as a JSONB field with a version number. Provide an admin API to upsert new versions without any schema migration required.

Contingency: If the versioning model is insufficient for a Bufdir schema change, fall back to a code deployment with the updated default schema, using the database config only for org-specific overrides.