Add cache invalidation strategy to TerminologyRepository
epic-dynamic-terminology-and-labels-foundation-task-007 — Extend TerminologyRepository with a staleness check: store a timestamp alongside the cached label map in TerminologyCacheAdapter. If the cache is older than a configurable TTL (default 24 hours), attempt a background refresh from Supabase. Expose an invalidateCache(orgId) method for forced refresh (used after admin label updates). Ensure the stale cache is still served while the refresh is in-flight.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
Pass a `DateTime Function() clock` parameter defaulting to `() => DateTime.now().toUtc()` to make the staleness check fully testable. For the background refresh, use `unawaited(Future(() => _refreshInBackground(orgId)))` — import `package:meta/meta.dart` for the `@visibleForTesting` annotation on the refresh method. Avoid using Timer for the background job; a plain unawaited Future is sufficient and easier to test. The stale-while-revalidate pattern is the intended UX: users always get a fast response, and the next app session will have fresh labels.
Document the TTL default (24h) in a class-level doc comment explaining the rationale (org label changes are infrequent admin operations).
Testing Requirements
Inject a fake clock (DateTime Function() nowFn) into TerminologyRepository constructor to make TTL logic deterministic in tests. Cover: (1) fresh cache (age < TTL) → returns cache, zero Supabase calls; (2) stale cache (age >= TTL) → returns stale value immediately, background fetch fires; (3) background fetch updates cache after completion; (4) background fetch failure → cache unchanged, no exception propagated; (5) invalidateCache() → clears entry, triggers foreground fetch, returns new value; (6) invalidateCache() with network failure → returns Left(failure), cache remains empty. Use flutter_test with mockito/mocktail. All tests must be deterministic — no real Durations or actual delays.
The labels JSONB column in organization_configs may lack a consistent schema across organizations, causing deserialization failures or silent missing keys when a new organization is onboarded with a differently structured map.
Mitigation & Contingency
Mitigation: Define and enforce a canonical JSONB schema via a Supabase check constraint and a migration script. Validate the schema in TerminologyRepository at parse time and emit structured errors for any key-type mismatches.
Contingency: If schema drift is discovered post-deployment, LabelKeyResolver's fallback logic ensures the app continues to function with English defaults while a data migration is prepared to normalize the offending organization's config.
Device local storage corruption or platform-specific SharedPreferences serialization bugs could render a cached terminology map unreadable, causing the app to fall back to English defaults unexpectedly for an organization with custom terminology.
Mitigation & Contingency
Mitigation: Wrap all cache reads in try/catch, validate the deserialized map against a minimum-required-keys check, and evict corrupted entries automatically before re-fetching from Supabase.
Contingency: Surface a non-blocking in-app warning to the coordinator that terminology may be in default English until the next sync completes; trigger an immediate background sync.