high priority low complexity backend pending backend specialist Tier 0

Acceptance Criteria

LocalDistanceCache exposes getDistance(String routeHash) → double? returning null if no entry exists or entry is expired
LocalDistanceCache exposes saveDistance(String routeHash, double distanceKm) → void persisting the value with a timestamp
LocalDistanceCache exposes invalidate(String routeHash) → void removing a specific cache entry
LocalDistanceCache exposes clearAll() → void removing all cache entries (for use on logout or org switch)
TTL is configurable at construction time with a default of 30 days
getDistance returns null for an entry whose stored timestamp is older than the TTL — the entry is also deleted at this point (lazy eviction)
saveDistance overwrites an existing entry for the same routeHash without error
routeHash is validated as non-empty string — empty string throws ArgumentError
distanceKm is validated as > 0 — zero or negative value throws ArgumentError
Cache reads are synchronous (no async/await in the public API) — SharedPreferences instance is pre-loaded at cache construction
Cache does not store raw route strings (origin/destination addresses) — only the hash and numeric distance to minimize PII storage
Unit tests pass with a mocked SharedPreferences (using shared_preferences FakeSharedPreferences test helper)

Technical Requirements

frameworks
Flutter
Riverpod
flutter_test
performance requirements
getDistance completes synchronously in under 1ms (SharedPreferences is pre-loaded)
saveDistance completes the in-memory write synchronously; the async SharedPreferences persist can be fire-and-forget
clearAll removes up to 200 cache entries in under 10ms
security requirements
Only the route hash (not the original origin/destination strings) is stored in SharedPreferences to avoid PII persistence on device
Cache entries are cleared on user logout — register a clearAll() call in the auth sign-out flow
SharedPreferences keys must be namespaced (e.g. distance_cache_v1_{routeHash}) to avoid collisions with other app preferences
Do not store distance values in plain text alongside identifiable route information

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use shared_preferences package (already a standard Flutter dependency). Store each cache entry as two keys: distance_cache_v1_{hash}_km (double as string) and distance_cache_v1_{hash}_ts (int epoch milliseconds). This avoids JSON serialization overhead for a simple two-field structure. For synchronous reads, require the SharedPreferences instance to be passed in at construction (constructor injection) — the caller is responsible for awaiting SharedPreferences.getInstance() before constructing LocalDistanceCache.

This keeps the public API synchronous while remaining testable. The Riverpod provider for this class should be a FutureProvider that resolves after SharedPreferences.getInstance() completes, then exposes the constructed LocalDistanceCache synchronously.

Testing Requirements

Write unit tests using flutter_test with SharedPreferences.setMockInitialValues() to simulate pre-populated cache state. Cover: (1) getDistance returns null for missing key; (2) saveDistance then getDistance returns correct value; (3) saveDistance then getDistance after TTL expiry returns null and removes entry; (4) invalidate removes only the targeted entry; (5) clearAll removes all namespaced entries without affecting unrelated SharedPreferences keys; (6) saveDistance with distanceKm <= 0 throws ArgumentError; (7) getDistance with empty routeHash throws ArgumentError; (8) two saves with same routeHash — second overwrites first. Target 100% branch coverage.

Component
Local Distance Cache
infrastructure low
Epic Risks (3)
high impact medium prob security

Supabase Row Level Security policies for mileage_claims may require complex join conditions to distinguish peer mentor (own claims only) from coordinator (chapter-scoped claims) access. If the RLS policy is misconfigured, coordinators could see claims outside their chapter scope or peer mentors could read other users' data, causing a data privacy incident.

Mitigation & Contingency

Mitigation: Write RLS policy SQL as part of this epic with explicit test cases for each role. Use Supabase's built-in policy testing tools and add integration tests that assert cross-user data isolation before merging.

Contingency: If RLS configuration proves too complex to test reliably within the epic, add an application-layer guard in the adapter that filters query results by authenticated user ID as a defence-in-depth measure while the policy is corrected.

medium impact low prob scope

Norwegian tax authority reimbursement rounding rules may change or may not be publicly documented in machine-readable form. Using the wrong rounding convention could cause systematic over- or under-payment, leading to compliance issues for the organisation.

Mitigation & Contingency

Mitigation: Source the exact rounding specification from HLF's finance team before implementing MileageCalculationService. Document the rule as a comment in the source code and link to the authoritative reference.

Contingency: If the rule is ambiguous, implement both truncation and half-up rounding behind a configuration flag so the organisation can switch without a code release.

low impact low prob technical

SharedPreferences reads and writes are asynchronous. If the distance prefill service (built in the next epic) calls LocalDistanceCache concurrently with a post-submission write, a race condition could result in the cache returning a stale or partially written value, causing the next form load to show an incorrect default.

Mitigation & Contingency

Mitigation: Wrap all SharedPreferences access in LocalDistanceCache with sequential async operations and document the non-concurrent usage contract in the class API.

Contingency: If race conditions are observed in testing, introduce a simple mutex pattern using a Completer to serialise cache operations.