high priority low complexity frontend pending frontend specialist Tier 5

Acceptance Criteria

A Riverpod `AsyncNotifierProvider` (or `FutureProvider.family`) named `claimEventsProvider` accepts a `claimId` parameter and returns `AsyncValue<List<ClaimEvent>>`
The provider calls `ClaimEventsRepository.getEventsForClaim(claimId)` and the repository queries Supabase with `.order('created_at', ascending: true)`
Loading state is exposed as `AsyncValue.loading()` before the first data arrives
Network or Supabase errors are exposed as `AsyncValue.error(e, st)` — never swallowed silently
Data state delivers events in strictly ascending chronological order (verified by test assertions on `created_at`)
If the claim has no events, the provider returns `AsyncValue.data([])` (empty list) — not an error
Provider is invalidated and re-fetched when `ref.invalidate(claimEventsProvider(claimId))` is called
Provider uses `autoDispose` to release resources when no widget is listening
Repository method is unit-testable via constructor-injected Supabase client mock

Technical Requirements

frameworks
Flutter (Dart)
Riverpod (flutter_riverpod, riverpod_annotation for code generation optional)
Supabase Flutter SDK (supabase_flutter)
apis
Supabase REST: SELECT * FROM claim_events WHERE claim_id = :id ORDER BY created_at ASC
ClaimEventsRepository.getEventsForClaim(String claimId) → Future<List<ClaimEvent>>
data models
ClaimEvent (claim_id, event_id, state_transition, actor_id, actor_display_name, actor_role, coordinator_comment, created_at)
ClaimEventStateTransition (enum: submitted, approved, rejected, escalated, auto_approved)
performance requirements
First data frame renders within 200 ms of provider activation on a normal network
Provider must not re-fetch on every rebuild — use autoDispose + keepAlive if the timeline is frequently revisited
security requirements
Supabase RLS policy must restrict claim_events rows to the authenticated user's accessible claims — the repository must not add client-side filtering as a security measure
ClaimEvent must not expose raw actor_id (UUID) in the UI layer — use actor_display_name from the record

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Create `ClaimEventsRepository` as a plain Dart class with a constructor-injected `SupabaseClient`. Keep the repository thin — only Supabase query + JSON deserialization. Do NOT add business logic to the repository. Define the Riverpod provider in a separate `claim_events_provider.dart` file, not inside the widget file.

Use `.family` modifier to parameterize by `claimId`. For `ClaimEvent.fromJson()`, use strict field mapping — do not use dynamic keys. Ensure `created_at` is parsed as `DateTime` in UTC, then converted to local timezone only at the display layer (task-015). Follow the existing Riverpod provider pattern in the codebase for consistency.

Testing Requirements

Unit tests using `flutter_test` with a mocked `ClaimEventsRepository` (using `mocktail` or manual fake). Test cases: (1) provider emits loading then data with events in ascending order; (2) provider emits loading then error when repository throws; (3) provider returns empty list when repository returns []; (4) provider re-fetches after invalidation. Use `ProviderContainer` directly in tests — no widget needed. Assert on `AsyncValue` states using `container.read(claimEventsProvider(testClaimId))`.

All tests must be deterministic — use fixed timestamps in test data.

Component
Claim Status Audit Timeline
ui low
Epic Risks (3)
high impact high prob technical

The ThresholdEvaluationService is described as shared Dart logic used both client-side and in the Edge Function. Supabase Edge Functions run Deno/TypeScript, not Dart, meaning the threshold logic must be maintained in two languages and can diverge, causing the server to reject legitimate client submissions.

Mitigation & Contingency

Mitigation: Implement the threshold logic as a single TypeScript module in the Edge Function and call it via a thin Dart HTTP client wrapper for client-side preview feedback only. The server is always authoritative; the client version is purely for UX (showing the user whether their claim will auto-approve before they submit).

Contingency: If dual-language maintenance is unavoidable, create a shared golden test file (JSON fixtures with inputs and expected outputs) that is run against both implementations in CI to detect divergence immediately.

medium impact medium prob technical

A peer mentor could double-tap the submit button or a network retry could trigger a duplicate submission, causing the ApprovalWorkflowService to attempt two concurrent state transitions from draft→submitted for the same claim, potentially resulting in two audit events or conflicting statuses.

Mitigation & Contingency

Mitigation: Implement idempotency in the ApprovalWorkflowService using a database-level unique constraint on (claim_id, from_status, to_status) per transition, combined with a UI-level submission lock (disable button after first tap until response returns).

Contingency: Add a deduplication check at the start of every state transition method that returns the existing state if an identical transition is already in progress or completed within the last 10 seconds.

high impact medium prob scope

Claims with multiple expense lines (e.g., mileage + parking) must have their combined total evaluated against the threshold. If individual lines are added asynchronously or the evaluation runs before all lines are persisted, the auto-approval decision may be computed on an incomplete set of expense lines.

Mitigation & Contingency

Mitigation: The Edge Function always fetches all expense lines from the database (not from the client payload) before computing the threshold decision. Define a clear claim submission contract that requires all expense lines to be persisted before the submit action is called.

Contingency: Add a validation step in ApprovalWorkflowService that counts expected vs. persisted expense lines before allowing the transition, returning a validation error if lines are missing.