critical priority medium complexity backend pending backend specialist Tier 1

Acceptance Criteria

OrganizationLabelsNotifier extends StateNotifier<TerminologyState> and is injectable via Riverpod
On construction, notifier immediately fetches the active org's terminology map from TerminologyRepository using the injected orgId
Fetched map is written to TerminologyCacheAdapter before state is updated to loaded
State transitions through TerminologyState.loading → TerminologyState.loaded(map) or TerminologyState.error(message) correctly
The synchronous label(String key) helper returns the resolved label string when the map is loaded and the key exists
label(key) returns the key itself as a fallback when the map is not loaded, the key is absent, or an error occurred — never returns null or throws
On fetch failure, the notifier exposes TerminologyState.error and schedules one automatic retry after a configurable delay (default 3 s)
After 3 consecutive failures the notifier stops retrying and remains in error state; the error is surfaced via the state, not thrown
All Supabase calls go exclusively through TerminologyRepository — no direct Supabase client usage in the notifier
TerminologyCacheAdapter.write() is called once and only once per successful fetch, with the full map and a server-provided updatedAt timestamp
Unit tests verify each state transition: loading, loaded, error, retry, and fallback label resolution

Technical Requirements

frameworks
Flutter
Riverpod
flutter_riverpod
apis
TerminologyRepository.fetchMap(orgId)
TerminologyCacheAdapter.write(orgId, map, updatedAt)
data models
TerminologyMap (Map<String, String>)
TerminologyState (sealed/union: loading | loaded | error)
OrganizationId
performance requirements
label(key) must be O(1) — backed by an in-memory Map, never triggers I/O
Initial fetch must complete within 2 s on a standard mobile connection
Cache write must be non-blocking from the notifier's perspective (fire-and-forget with error logging)
security requirements
orgId must be read from the authenticated session scope — never accepted as a raw constructor parameter from untrusted callers
No terminology keys or values are logged at info level; debug-level logging only, guarded by kDebugMode

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Model TerminologyState as a sealed class (Dart 3) with three variants: loading(), loaded(Map map), and error(String message, int attempt). Store the in-memory map as a nullable private field alongside the Riverpod state so label() can access it synchronously without pattern-matching. Use a late-cancelled Timer for retry scheduling; cancel it in dispose() to prevent leaks. Keep the retry counter in instance state, not in TerminologyState, so UI is not burdened with retry internals.

The repository call should be wrapped in a try/catch that converts both SocketException and Supabase PostgrestException into the error state. Prefer async/await over raw Future.then chaining for readability. Do not use BuildContext inside the notifier.

Testing Requirements

Unit tests using flutter_test and Riverpod's ProviderContainer. Mock TerminologyRepository to simulate success, first-attempt failure with retry success, and three consecutive failures. Assert state sequence for each scenario using expectLater + emitsInOrder. Test label(key) for: key present in loaded state, key absent in loaded state (returns key), called in loading state (returns key), called in error state (returns key).

Achieve 100% branch coverage on all state transitions. No integration or widget tests required for this unit.

Component
Organization Labels Notifier
service medium
Epic Risks (3)
high impact medium prob technical

When a user switches organization context (e.g., a coordinator with multi-org access), a race condition between the outgoing organization's map disposal and the incoming organization's fetch could briefly expose the wrong organization's terminology to the widget tree.

Mitigation & Contingency

Mitigation: Implement an explicit loading state in OrganizationLabelsNotifier that widgets check before rendering any resolved labels. The provider graph should cancel the previous organization's fetch via Riverpod's ref.onDispose before initiating the next.

Contingency: If the race manifests in production, fall back to English defaults during the transition window and emit a Sentry error event for investigation; the UX impact is a brief English flash rather than wrong-org terminology.

high impact low prob security

Supabase Row Level Security policies on organization_configs may inadvertently restrict the authenticated user from reading their own organization's labels JSONB column, causing silent empty maps that appear as English fallbacks.

Mitigation & Contingency

Mitigation: Write and test explicit RLS policies that grant SELECT on the labels column to any authenticated user whose organization_id matches. Add an integration test that verifies label fetch succeeds for each role (peer mentor, coordinator, admin).

Contingency: If RLS blocks are discovered in production, temporarily escalate label fetch to a service-role edge function while the RLS policy is corrected, ensuring no labels are exposed cross-organization.

medium impact medium prob scope

A peer mentor who installs the app for the first time with no internet connection will have no cached terminology map and will see only English defaults, which may be confusing for organizations like NHF that use Norwegian-specific role names exclusively.

Mitigation & Contingency

Mitigation: Bundle a default fallback terminology map for each known organization as a compile-time asset (Dart asset file) so that even fresh installs without connectivity render correct organizational terminology immediately.

Contingency: If bundled assets are out of date, display a one-time informational banner noting that terminology will update on next connectivity restore, with no functional blocking of the app.