critical priority medium complexity backend pending backend specialist Tier 3

Acceptance Criteria

DeclarationAuditLogger class exists at lib/features/declarations/data/audit/declaration_audit_logger.dart with a corresponding abstract interface IDeclarationAuditLogger
logDeclarationSent(declarationId, orgId, {Map<String, dynamic>? metadata}) → Future<void> inserts a row with event_type='sent'
logDeclarationOpened(declarationId, orgId, {Map<String, dynamic>? metadata}) → Future<void> inserts a row with event_type='opened'
logDeclarationAcknowledged(declarationId, orgId, {Map<String, dynamic>? metadata}) → Future<void> inserts a row with event_type='acknowledged'
logDeclarationExpired(declarationId, orgId, {Map<String, dynamic>? metadata}) → Future<void> inserts a row with event_type='expired'
logDeclarationRevoked(declarationId, orgId, {Map<String, dynamic>? metadata}) → Future<void> inserts a row with event_type='revoked'
No update, delete, patch, or modify methods are exposed — the public API is insert-only
If the Supabase INSERT fails due to network error, the event is buffered locally in an in-memory queue and retried on next app resume or after a 30-second backoff
If the buffer exceeds 50 pending events, the oldest events are flushed to local SQLite storage for persistence across app restarts
All methods catch Supabase errors and throw AuditLogException with a descriptive message — callers are never exposed to raw Supabase errors
actor_id is always derived from supabase.auth.currentUser!.id internally — callers must not supply actor_id
Logger is registered as a Riverpod provider and injectable via ref.watch(declarationAuditLoggerProvider)
Unit test coverage ≥90% on the logger class using mocked Supabase client

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
dart:async (for retry/buffering logic)
apis
Supabase PostgREST API (declaration_audit_log INSERT)
data models
AuditLogEntry (Dart model: id, eventType, declarationId, actorId, orgId, occurredAt, metadata)
AuditEventType (enum: sent, opened, acknowledged, expired, revoked)
performance requirements
Each log method must complete in <200ms on a normal network — fire-and-forget pattern acceptable if the caller does not need to await confirmation
Local buffer retry must not block the UI thread — all retry logic runs on a background isolate or via Future.microtask
security requirements
metadata parameter must be sanitized — no raw user input passed directly; only structured keys (e.g., {'template_version': '1.2'}) are accepted
actor_id must be resolved from the authenticated Supabase session internally — never accepted as a parameter to prevent impersonation
Log entries must not contain unencrypted PII in the metadata field — only reference IDs and event context

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

The fire-and-forget vs await decision: for audit logging, prefer unawaited(logger.logDeclarationSent(...)) at the call sites in the repository/service layer so that audit failures never block the main user action. However, the logger itself should still be async internally to handle retry.

Implement the local buffer as a simple List held in the logger instance, with a periodic timer (every 30 seconds) that attempts to flush. For the persistence fallback (>50 events), use shared_preferences for simplicity (serialize as JSON) rather than a second SQLite database — the goal is survival across a single app crash, not long-term offline storage. The abstract interface IDeclarationAuditLogger makes testing easy: the BLoC and service layers depend on the interface, not the concrete class. Riverpod registration should use a Provider so the mock can be injected in tests via ProviderScope overrides.

Ensure the AuditEventType enum values map exactly to the Postgres enum strings ('sent', 'opened', etc.) via a toSupabaseString() extension method.

Testing Requirements

Unit tests using flutter_test with mocked Supabase client (mocktail): (1) each log method inserts a row with the correct event_type string; (2) actor_id in the inserted row matches supabase.auth.currentUser.id; (3) a Supabase network error causes the event to be added to the local buffer; (4) after network recovery, buffered events are retried and inserted; (5) buffer overflow (>50 events) triggers persistence to local storage; (6) no update or delete method exists on the public interface (verified by reflection or code review check); (7) AuditLogException is thrown (not rethrown raw) on Supabase error. Integration test against local Supabase emulator: full lifecycle log (sent → opened → acknowledged) produces 3 immutable rows in declaration_audit_log.

Component
Declaration Audit Logger
infrastructure medium
Epic Risks (3)
high impact medium prob security

Row-level security policies for driver assignments and declarations must correctly scope data to the coordinator's chapter without leaking records across organizations. An incorrect RLS predicate could silently return empty result sets or, worse, expose cross-org data, both of which are difficult to detect in unit tests.

Mitigation & Contingency

Mitigation: Write dedicated RLS integration test scenarios with multiple org fixtures asserting both data isolation and correct data visibility. Use Supabase's built-in policy testing utilities and review policies with a second developer.

Contingency: If RLS policies prove too complex to get right quickly, implement application-layer org scoping as a temporary guard while RLS is fixed in a follow-up, with an explicit security review gate before production deployment.

high impact medium prob security

The declaration audit logger must produce tamper-evident records. If the database allows updates or deletes on audit rows, the compliance guarantee is broken. Supabase does not natively prevent row deletion by default.

Mitigation & Contingency

Mitigation: Implement an insert-only RLS policy on the audit table that denies UPDATE and DELETE for all roles including the service role. Add a database trigger that rejects mutation attempts and logs the attempt itself.

Contingency: If immutability cannot be enforced at the database level within the sprint, store audit entries in an append-only Supabase Edge Function log stream as a temporary alternative, with a migration plan to the proper table once constraints are implemented.

medium impact low prob technical

The org-feature-flag-service caches flag values to avoid repeated database reads. If the cache is not invalidated promptly after an admin toggles the flag, coordinators may see stale UI state — either seeing driver features when they should not, or not seeing them when they should.

Mitigation & Contingency

Mitigation: Use a Supabase Realtime subscription to listen for changes on the driver_feature_flag_config table and invalidate the in-memory cache immediately on change. Set a short TTL (60 seconds) as a safety net.

Contingency: If Realtime subscription proves unreliable, expose a manual cache-bust endpoint accessible from the admin toggle action, ensuring the cache is cleared synchronously on every flag change.