high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

On first successful fetchAggregatedData call, the result is persisted to local cache keyed by (periodId, orgId)
On subsequent fetchAggregatedData calls within the TTL window, the stream emits cached data immediately before initiating a background network refresh
After a background refresh completes successfully, the stream emits the updated data and overwrites the cache
When the TTL has expired, the repository treats the cache as stale and fetches fresh data before emitting
Changing the periodId invalidates the cache for the previous period and does not serve stale data for the new period
Switching the authenticated organisation invalidates all cached entries for the previous org
Cache entries are stored as JSON strings (not raw Dart objects) to survive app restarts
The TTL is configurable via a constructor parameter with a sensible default (e.g., 15 minutes)
When the device is offline and cached data exists within TTL, the stream emits cached data with an AsyncData state — not an error
When the device is offline and no cached data exists, the stream emits AsyncError with a typed NetworkUnavailableException

Technical Requirements

frameworks
Flutter
Riverpod
Hive or shared_preferences
apis
Hive (hive_flutter) or shared_preferences Dart package
BufdirReportData.toJson() / fromJson()
Dart connectivity_plus for offline detection
BufdirPreviewRepository existing fetch methods
data models
BufdirReportData
BufdirCacheEntry (wraps BufdirReportData + cachedAt timestamp + periodId + orgId)
BufdirRepositoryException
NetworkUnavailableException
performance requirements
Cache read must complete synchronously or within 50 ms — Hive box reads are synchronous after box is open
Background refresh must not block the UI — run in a separate Future chain that updates the stream when complete
Cache write must be fire-and-forget (unawaited) so it does not delay the stream emission to the UI
security requirements
Cached report data must be stored with AES encryption if Hive is used (hive_flutter supports encrypted boxes) — Bufdir report data may contain sensitive financial aggregates
Cache encryption key must be derived from the user's session, not hardcoded — use flutter_secure_storage to store the key
Cache must be fully cleared on logout — implement a clearAll() method called by the auth logout handler

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Introduce a BufdirCacheEntry wrapper class serialisable to JSON: { 'data': BufdirReportData.toJson(), 'cachedAt': ISO8601 string, 'periodId': String, 'orgId': String }. Cache key format: 'bufdir_preview_${orgId}_${periodId}'. Use Hive over shared_preferences if the data volume per entry exceeds 1 KB — Hive handles larger values more efficiently and supports type adapters. For the stale-while-revalidate pattern: use a StreamController that first adds the cached AsyncData, then triggers the network call and adds updated AsyncData or AsyncError when it resolves.

Abstract the storage layer behind a BufdirCacheStore interface so tests can inject an in-memory implementation without Hive/shared_preferences. Register the encryption key retrieval in a separate SecureStorageService to keep the repository focused on data concerns.

Testing Requirements

Unit tests using flutter_test and mocktail: (1) fresh cache miss triggers network fetch and stores result, (2) cache hit within TTL emits cached data and triggers background refresh, (3) background refresh on cache hit emits updated data after completion, (4) expired cache triggers network fetch (not cache), (5) period change invalidates old cache entry, (6) org switch clears all cache entries, (7) offline with valid cache emits AsyncData, (8) offline with no cache emits AsyncError with NetworkUnavailableException. Mock the storage adapter (Hive box or shared_preferences) using an in-memory fake. Mock connectivity_plus with a FakeConnectivity. Use fakeAsync to advance TTL without real time delays.

Component
Bufdir Preview Repository
data medium
Epic Risks (2)
high impact medium prob integration

The preview repository depends on aggregated data produced by the Bufdir Data Aggregation feature. If the aggregation RPC schema or Supabase view columns change during parallel development, the repository's typed Dart models will break, causing compile errors or runtime null-dereference failures.

Mitigation & Contingency

Mitigation: Define a shared Dart interface (abstract class) for the aggregated data contract early and have both features code against it. Use Supabase typed generated clients so schema mismatches surface at code generation time rather than runtime.

Contingency: If the aggregation schema changes after the repository is complete, run `supabase gen types dart` immediately, update the repository model, and run repository unit tests before unblocking UI development. Keep a mock data fixture so UI work can continue during the fix.

high impact medium prob scope

The BufdirReportStructureMapper must map internal activity category IDs to the exact label strings used on the official Bufdir reporting form. If the mapping is incomplete or uses outdated labels, coordinators will see mismatches when cross-referencing the preview with the paper form, potentially leading to incorrect submissions.

Mitigation & Contingency

Mitigation: Obtain the current Bufdir reporting form PDF directly from Bufdir (Norse Digital Products has an existing Bufdir dialogue). Extract all field labels and section names into a static constants file reviewed by at least one coordinator from NHF or HLF before implementation begins.

Contingency: If incorrect labels are discovered during UAT on TestFlight, update the static constants file and redeploy. Because the mapper is a pure Dart class with no database storage, corrections require no migration — only a new build.