high priority medium complexity backend pending backend specialist Tier 2

Acceptance Criteria

StatsAsyncNotifier reads the resolved ChapterScope via ref.watch(chapterScopeResolverProvider) inside build()
The resolved ChapterScope is passed as a filter argument to the stats repository query alongside the time window
When the user is a coordinator, the repository receives a chapter-scoped filter and the returned StatsSnapshot includes all peer mentors in that chapter
When the user is a peer mentor, the repository receives a self-only filter and the returned StatsSnapshot contains only that peer mentor's own data
A scope change (e.g., coordinator switches chapter) triggers ref to re-run build() and produce a fresh AsyncData with the new scope's data
If ChapterScopeResolver throws AuthorizationException, StatsAsyncNotifier state becomes AsyncError with that exception — it does not swallow it
Unit tests cover: coordinator scope produces chapter-wide snapshot, peer mentor scope produces single-user snapshot, scope change triggers re-fetch, AuthorizationException propagates to AsyncError
No hardcoded role strings in the notifier — role discrimination is delegated entirely to ChapterScopeResolver

Technical Requirements

frameworks
flutter
riverpod (AsyncNotifier)
flutter_riverpod
apis
ChapterScopeResolver (provider)
StatsRepository (with scope filter parameter)
data models
ChapterScope
StatsSnapshot
TimeWindow
performance requirements
Scope resolution must not add more than 50ms to the total fetch time when the cache is warm
Chapter switch must complete the full re-fetch cycle in under 3 seconds on a 4G connection
security requirements
The notifier must never pass a coordinator-level scope to the repository when the authenticated user is a peer mentor — ChapterScopeResolver is the single authority
Repository RLS policies are the last line of defense; the notifier scope filter is defense-in-depth only

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

ChapterScope should be a sealed class or enum with two variants: ChapterScope.chapter(String chapterId) and ChapterScope.self(String userId) — this avoids nullable/optional ambiguity. If ChapterScopeResolver is itself an AsyncNotifier, use ref.watch and handle the AsyncValue — do not call .requireValue which throws on loading/error states. Add a guard: if chapterScopeProvider is in AsyncLoading state, StatsAsyncNotifier should remain in AsyncLoading rather than fetching with an incomplete scope. Keep the scope wiring inside build() alongside the time window watch — both dependencies are declared in one place for clarity.

Testing Requirements

Unit tests using flutter_test and ProviderContainer. Mock both ChapterScopeResolver and StatsRepository. Test cases: (1) Coordinator role → chapterScopeResolverProvider resolves to ChapterScope.chapter(chapterId) → repository called with chapter filter → snapshot contains multiple peer mentors. (2) Peer mentor role → scope resolves to ChapterScope.self(userId) → repository called with self filter → snapshot contains only one peer mentor summary.

(3) Scope changes from chapter A to chapter B → build() re-runs → repository called with new chapter filter. (4) ChapterScopeResolver throws AuthorizationException → state becomes AsyncError. Verify no direct role-checking logic exists inside the notifier itself.

Component
Stats Async Notifier
service medium
Epic Risks (2)
medium impact medium prob technical

Supabase realtime channel subscriptions that are not properly disposed on screen close can accumulate in memory across navigation events, causing duplicate invalidation calls, ghost fetches, and eventual memory leaks on long sessions.

Mitigation & Contingency

Mitigation: Implement StatsCacheInvalidator as a Riverpod provider with an explicit ref.onDispose callback that cancels the realtime channel subscription. Write a widget test that navigates away and back multiple times and asserts that only one subscription is active at any given time.

Contingency: If subscription leaks are found in production, add a global subscription registry that enforces at-most-one subscription per channel key, and schedule a dispose sweep on app background events.

medium impact low prob scope

Debouncing rapid inserts may swallow the invalidation signal if the debounce window outlasts the Supabase realtime event delivery window, resulting in the dashboard showing stale totals after a bulk registration completes.

Mitigation & Contingency

Mitigation: Set the debounce window to 800ms (shorter than the typical Supabase realtime delivery latency of 1-2s for batched events) and ensure the leading-edge invalidation fires immediately while trailing duplicates are suppressed. Integration-test with a 20-record bulk insert.

Contingency: If debounce timing proves unreliable, replace debounce with a trailing-edge timer reset on each event and add a guaranteed invalidation 5 seconds after the last event regardless of subsequent events.