critical priority medium complexity backend pending backend specialist Tier 4

Acceptance Criteria

ActivityTypeCacheProvider is defined as an AsyncNotifierProvider<ActivityTypeCacheNotifier, List<ActivityType>> using Riverpod code generation annotations
On first read, the provider calls ActivityTypeRepository.fetchActiveByOrganisation(orgId) exactly once and stores the result in state
Subsequent reads within the same session return the in-memory list without triggering any Supabase network calls (verified via mock call count assertions)
Provider is scoped to the current organisation session: switching organisation invalidates and re-fetches
Provider exposes only activity types where is_active == true; soft-deleted types are excluded
Provider state transitions correctly through loading → data → error states and exposes AsyncValue to consumers
When the repository throws a typed ActivityTypeException, the provider state becomes AsyncError with that exception type preserved
Provider is registered in the correct ProviderScope layer so it is accessible from activity registration wizard, expense eligibility checks, and Bufdir export screens
No memory leaks: provider disposes correctly when the ProviderScope it belongs to is disposed
Thread safety: concurrent reads during the initial fetch do not result in duplicate network calls (single-flight pattern or Riverpod's built-in deduplication)

Technical Requirements

frameworks
Riverpod (riverpod_annotation + riverpod_generator for code-gen)
Flutter
freezed (for immutable ActivityType model if not already defined)
apis
ActivityTypeRepository.fetchActiveByOrganisation(String orgId)
Supabase PostgREST via repository abstraction (not called directly)
data models
activity_type (id, organization_id, name, description, is_travel_expense_eligible, is_active)
performance requirements
Initial fetch must complete within 2 seconds on a standard 4G connection
In-memory reads must return synchronously (< 1 ms) after first fetch
Provider must not re-fetch on every widget rebuild — cache persists across rebuilds for the session lifetime
Total cached payload should not exceed 500 KB; if org has > 500 activity types, apply pagination strategy
security requirements
Cached list must be org-scoped: provider must read orgId from the authenticated session JWT claim, never from unvalidated client state
ActivityType data must not be cached across organisation switches to prevent cross-org data leakage
Provider must not expose any write operations — it is read-only; mutations flow exclusively through the repository

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Use @riverpod annotation with keepAlive: true on the notifier to prevent auto-disposal during navigation. The orgId should be read from an authSessionProvider (already established in the project) rather than passed as a parameter, to avoid provider family complexity. Implement the single-flight guard by checking if state is already AsyncData before calling the repository — Riverpod's AsyncNotifier handles this naturally when build() is only called once. Keep the provider thin: no business logic, no filtering beyond is_active — all transformation belongs in the metadata resolver (task-011).

Annotate with @riverpod and run build_runner to generate the .g.dart file; never hand-write the generated code.

Testing Requirements

Unit tests using flutter_test and mocktail. Test cases: (1) first access triggers exactly one repository.fetchActiveByOrganisation call; (2) second access returns cached result with zero additional repository calls; (3) AsyncValue is AsyncLoading on construction before future resolves; (4) AsyncValue becomes AsyncData> on success; (5) AsyncValue becomes AsyncError when repository throws ActivityTypeNotFoundException; (6) only is_active==true types appear in the returned list; (7) provider state is empty list (not error) when org has no active types. Use ProviderContainer for isolated provider testing without Flutter widget tree.

Component
Activity Type Cache Provider
data medium
Epic Risks (3)
high impact medium prob technical

The JSONB metadata column has no enforced schema at the database level. If the Dart model and the stored JSON diverge (e.g., a field is renamed or a new required flag is added without a migration), the metadata resolver will silently return null or throw at parse time, breaking conditional wizard logic for all organisations.

Mitigation & Contingency

Mitigation: Define a versioned Dart Freezed model for ActivityTypeMetadata and add a Supabase check constraint or trigger that validates the JSONB structure on write. Document the canonical metadata schema in a shared constants file and require schema review for any metadata field additions.

Contingency: Implement a lenient parse path in ActivityTypeMetadataResolver that returns safe defaults for missing fields and logs a structured warning to Supabase edge logs, allowing the app to degrade gracefully rather than crash.

high impact low prob security

If RLS policies on the activity_types table are misconfigured, a coordinator from one organisation could read or mutate activity types belonging to another organisation, violating data isolation guarantees required by all three client organisations.

Mitigation & Contingency

Mitigation: Write integration tests against the Supabase local emulator that explicitly assert cross-org isolation: a token scoped to org A must receive zero rows when querying org B activity types, and upsert attempts must return permission-denied errors.

Contingency: Apply an emergency RLS policy patch via Supabase dashboard without a code deploy. Audit all activity_type rows for cross-org contamination and restore from backup if any data leakage is confirmed.

medium impact medium prob integration

If the cache invalidation call in ActivityTypeService is not reliably triggered after an admin creates, edits, or archives an activity type, peer mentors on the same device will see stale data in the registration wizard until the next app restart, leading to confusion and potential misregistrations.

Mitigation & Contingency

Mitigation: Enforce a strict pattern: ActivityTypeService always calls cacheProvider.invalidate() inside the same try block as the successful Supabase mutation, before returning to the caller. Write a widget test that verifies the cache notifier emits an updated list after a service mutation.

Contingency: Add a background Supabase Realtime subscription on the activity_types table that triggers cache invalidation automatically, providing an independent safety net independent of the service call path.