critical priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

ActivityTypeCacheNotifier extends StateNotifier<Map<String, _CacheEntry>> where _CacheEntry holds List<ActivityType> and a DateTime timestamp
getActiveTypes(orgId) returns cached list if entry exists and age < TTL; otherwise fetches from repository and stores result with current timestamp
TTL is configurable at construction time with a default of 10 minutes (Duration(minutes: 10))
invalidate(orgId) removes the cache entry for the given orgId, forcing the next getActiveTypes call to fetch fresh data
invalidateAll() clears the entire cache map (used on logout/org switch)
Cache is keyed by orgId string — different orgs never share cache entries
On repository fetch error in getActiveTypes, the exception propagates to the caller (no stale cache served on error)
Riverpod provider activityTypeCacheProvider is defined and returns the ActivityTypeCacheNotifier
State updates (cache population, invalidation) trigger Riverpod rebuild for widgets watching the provider
Unit test: second call to getActiveTypes within TTL does NOT call repository.fetchActiveByOrg a second time
Unit test: call after TTL expiry calls repository.fetchActiveByOrg again
Unit test: invalidate followed by getActiveTypes calls repository.fetchActiveByOrg again

Technical Requirements

frameworks
Flutter
Riverpod
Dart
data models
activity_type
performance requirements
Cache lookup must be O(1) HashMap lookup — do not iterate the full map
TTL check must use monotonic clock (Stopwatch or DateTime.now()) — not wall clock that can be manipulated
Cache memory footprint bounded: typical org has < 50 activity types; no size limit enforcement needed at this scale
security requirements
Cache must be cleared on user logout to prevent data leakage between sessions
Cache must be cleared on org switch to prevent cross-org data exposure
Cache entries are per-orgId — never serve one org's data to another org

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Place in lib/features/activity_type/data/cache/activity_type_cache_notifier.dart. Use a private _CacheEntry record/class with List data and DateTime fetchedAt. TTL check: DateTime.now().difference(entry.fetchedAt) > ttl. Avoid using Timer for automatic expiry — lazy expiry on access is simpler and sufficient here.

For Riverpod, use StateNotifierProvider> and expose a convenience provider activityTypeListProvider(orgId) using .select() or a derived provider that calls getActiveTypes and returns AsyncValue>. Wire invalidateAll() to the Riverpod auth state change listener (log out event) to ensure clean state. Do not persist the cache to disk — it is purely in-memory for the session lifetime.

Testing Requirements

Unit tests using flutter_test with a mocked IActivityTypeRepository: (1) cache hit: getActiveTypes called twice within TTL invokes fetchActiveByOrg exactly once, (2) cache miss after TTL: mock DateTime injection confirms stale entry triggers re-fetch, (3) invalidate: after invalidate(orgId), next getActiveTypes calls fetchActiveByOrg, (4) invalidateAll: clears all orgs' entries, (5) different orgIds maintain separate cache entries — org A invalidation does not affect org B, (6) repository error in getActiveTypes propagates without caching a null/empty result, (7) concurrent calls (using Future.wait) do not result in double-fetching (optional but recommended). Use fake/mock clock injection for TTL tests to avoid real timer waits.

Component
Activity Type Service
service medium
Epic Risks (2)
medium impact medium prob scope

Metadata flag combination rules differ between organisations (e.g., Blindeforbundet honorarium thresholds, HLF mutual exclusion of km and transit). Encoding these as generic service-level validation may be insufficient, forcing organisation-specific branching inside the service that becomes unmaintainable as new organisations are onboarded.

Mitigation & Contingency

Mitigation: Model flag validation as a pure function that accepts an ActivityTypeMetadata object and an org configuration record, making org-specific rules data-driven rather than hardcoded. Establish the validation contract in the foundation epic so the service just delegates to the validator.

Contingency: Defer complex cross-flag validation to a lightweight edge function that can be updated without a mobile app release, accepting that initial validation in the mobile service layer is permissive and corrected server-side.

high impact low prob technical

Blindeforbundet users rely on VoiceOver and JAWS. If the selection screen is built with non-semantic widgets that fail accessibility audit late in the sprint, a significant rework of the widget tree may be required, blocking the registration wizard integration.

Mitigation & Contingency

Mitigation: Build the selection screen against the project's established accessibility design tokens and semantics wrapper conventions from the start. Run Flutter's semantic tree inspector and a manual VoiceOver pass before marking any widget task complete.

Contingency: Wrap all tappable items in the project's SemanticsWrapperWidget and schedule a dedicated accessibility review session with a screen reader user from Blindeforbundet before the epic is closed.